@siladev/qalam
v0.11.3
Published
Islamic content editor with Quran verse references
Readme
@siladev/qalam
Islamic content editor with Quran verse references, hadith, poetry, and inline quotes. Built on Tiptap/React, RTL-first.
Install
bun add @siladev/qalamEditor
import { QalamEditor } from "@siladev/qalam";
import "@siladev/qalam/styles.css";
// Load quran data first
import { loadQuranData } from "@siladev/qalam";
const data = await fetch("/quran-data.json").then(r => r.json());
loadQuranData(data);
function App() {
const [doc, setDoc] = useState(initialContent);
return (
<QalamEditor
content={doc} // initial content (remount with key to replace)
onChange={setDoc}
onChangeDebounceMs={200} // recommended for long-doc live previews
repeat={true} // enable repeat controls
enableBlockIds // optional stable ids for comments/cross-refs
quranPreviewMode="smart"
dir="rtl"
/>
);
}Qalam's editor extension ABI is Tiptap-based. Hosts can add generic hooks without forking core:
<QalamEditor
extensions={[myTiptapExtension]}
toolbarExtras={<button type="button">Insert ref</button>}
/>Quran Preview Modes
quranPreviewMode controls how much Quran text is mounted inside the editable
Tiptap DOM:
"smart"(default) — full for short refs, compact for long refs, expanded while active."compact"— range/chip only, fastest for large books."full"— always full Quran text in the editor for WYSIWYG use.
Renderer and exports always use full Quran text. Compact editor refs use a clipboard handler so copy/paste still emits full readable Quran text.
For book-sized documents, avoid resolving/rendering previews directly inside
every onChange. Use onChangeDebounceMs so Qalam does not serialize the whole
document on every keystroke, then debounce resolveAST(doc) in the host.
Block IDs
enableBlockIds assigns stable optional attrs.id values to addressable
blocks: paragraphs, headings, blockquotes, poems, and list items. Old docs
without IDs remain valid.
Page View
Layout attrs are optional. Missing layout means pageless editing and A4 export defaults.
doc.attrs = {
layout: {
mode: "page",
pagePreset: "a4", // a4 | a5 | letter | custom
orientation: "portrait",
margins: { top: 20, right: 20, bottom: 20, left: 20 }, // mm
},
};pageBreak is an additive block node for manual page breaks.
Renderer
import { QalamRenderer, resolveAST } from "@siladev/qalam";
const resolved = resolveAST(doc); // needs quran data loaded
<QalamRenderer doc={resolved} dir="rtl" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| doc | QalamDocResolved | — | Resolved doc (run resolveAST(doc) first) |
| dir | "rtl" \| "ltr" | "rtl" | Reading direction |
| className | string | — | Extra class on the wrapper |
| footnotes | boolean \| FootnoteConfig | false | Move long citations to footnotes — see below |
| quranScript | "hafs" \| "uthmani" | "hafs" | Which Qur'an script to render |
| linkResolver | QalamLinkResolver | identity/http(s) external | Resolve opaque/internal links |
| renderLink | QalamRenderLink | default <a> | React-only link rendering override |
Footnotes
Off by default — citations render inline as [المصدر]. Turn on to move long citations into a numbered list at the bottom (classical Arabic Islamic convention: (١) markers, Eastern Arabic numerals, short separator).
// Off (default)
<QalamRenderer doc={resolved} />
// On with defaults — threshold 12 chars, applies to hadith/quote/blockquote/poem
<QalamRenderer doc={resolved} footnotes />
// Tuned
<QalamRenderer
doc={resolved}
footnotes={{
threshold: 20, // citations > this many chars get footnoted
types: ["hadith", "blockquote"], // restrict to certain citation types
}}
/>
// Footnote everything
<QalamRenderer doc={resolved} footnotes={{ threshold: 0 }} />Config shape:
type FootnoteCitationType = "hadith" | "quote" | "blockquote" | "poem" | "quran";
interface FootnoteConfig {
threshold?: number; // default: 12
types?: FootnoteCitationType[]; // default: ["hadith","quote","blockquote","poem"] (quran excluded — refs are always short)
}How it renders:
- Citations longer than
thresholdare replaced inline by a small(١)marker. - All footnoted citations collect into an
<aside class="qalam-render-footnotes">at the end of the rendered output, with bidirectional anchor links. - For blockquote, the marker is appended to the last paragraph (no awkward block-level marker on its own line).
- For poem, the marker sits centered below the last bayt, outside the bayt layout.
- For hadith, the marker comes after
(grade), matching academic convention.
Qur'an script
quranScript controls which orthography verses render in:
"hafs"(default) — Hafs Smart rendering, requires the bundledHafsSmartfont."uthmani"— canonical Uthmani script with+ Arabic-Indic digit aya markers. Renders properly inAmiri Quran(open-source, available on Google Fonts),KFGQPC Uthmanic Script, orScheherazade New. CSS falls back through that font stack automatically.
<QalamRenderer doc={resolved} quranScript="uthmani" />Internal Links
Qalam stores app refs as normal link marks with opaque hrefs. Core does not know about Drshaal materials, attachments, questions, or series.
drshaal:material/13
drshaal:attachment/63
drshaal:question/41
drshaal:series/7Use the pattern <scheme>:<kind>/<opaque-id>. The id should be treated as an
opaque string, not a number, so hosts can use slugs, UUIDs, or database ids.
Unknown schemes are preserved by validation, rendering, HTML export, and DOCX
export. This keeps old documents and future host-owned references round-trippable.
Resolve them at the app boundary:
const linkResolver = (href: string) =>
href.startsWith("drshaal:material/")
? { href: `/materials/${href.slice("drshaal:material/".length)}` }
: { href };renderLink affects React rendering only. HTML/DOCX/PDF exports use
linkResolver. Qalam intentionally does not expose renderLinkHtml yet; if a
host needs custom exported anchor markup later, that should be added as an
additive export-specific hook.
For picker/autocomplete UX, use the optional satellite package:
import { QalamRefsProvider, QalamEditorWithRefs } from "@siladev/qalam-refs";
<QalamRefsProvider kinds={refKinds}>
<QalamEditorWithRefs />
</QalamRefsProvider>Make sure an Uthmani-capable font is loaded — the bundled stylesheet expects Amiri Quran (which you can load from Google Fonts):
<link href="https://fonts.googleapis.com/css2?family=Amiri+Quran&display=swap" rel="stylesheet">Markdown
Bidirectional JSON <-> Markdown conversion.
import { docToMarkdown, markdownToDoc, resolvedDocToMarkdown, markdownToResolvedDoc } from "@siladev/qalam";
// Source markdown (for editing, LLM pipelines)
const md = docToMarkdown(doc);
const doc = markdownToDoc(md);
// Resolved markdown (for rendering without quran data)
const compiledMd = resolvedDocToMarkdown(resolvedDoc);
const resolvedDoc = markdownToResolvedDoc(compiledMd);Markdown Syntax
Inline nodes
:quran[2:البَقَرَة:255]
:quran[2:البَقَرَة:255]{basmalah nocite}
:quran[2:البَقَرَة:255:3-7x3,256]{basmalah}
:hadith[narration ^^prophetic speech^^ text]{cite="البخاري" grade="صحيح"}
:quote[quoted text]{cite="المصدر"}Marks
**bold** *italic* [link](url)
^^prophetic speech^^
{x3:repeated text}Export
DOCX and print/PDF export operate on a resolved document.
import { qalamToDocx, qalamToPrintHtml, qalamToPdf, resolveAST } from "@siladev/qalam";
const resolved = resolveAST(doc);
// A4 DOCX bytes
const docx = qalamToDocx(resolved, {
title: "My document",
footnotes: { threshold: 20 }, // long citations become native DOCX footnotes
bodyFont: "Times New Roman", // default; use "Amiri" only when target systems have it installed
});
// A4 HTML suitable for browser print/PDF flows
const html = qalamToPrintHtml(resolved, {
title: "My document",
footnotes: { threshold: 20 },
layout: { mode: "page", pagePreset: "a4" },
});
// Browser/headless PDF bytes via a caller-provided renderer, e.g. html2pdf.js/jsPDF.
const pdf = await qalamToPdf(resolved, {
renderHtmlToPdf: async (html) => {
// return Blob, Uint8Array, or ArrayBuffer
},
});Export notes:
- DOCX uses A4 by default. Normal prose defaults to
Times New Romanfor Word/LibreOffice compatibility; passbodyFontto target an installed font such asAmiri. - DOCX
footnotesare native Word footnotes, so Word/LibreOffice place them per page. linkResolveris applied to HTML and DOCX hyperlinks.pageBreakexports to CSS page breaks and DOCX page breaks.- HTML/PDF
footnotesare document-end notes. True per-page PDF footnotes require a pagination engine and are intentionally not faked. openQalamPrintWindow(resolved)is available for the browser print dialog path.
Blocks
# Heading 1
## Heading 2
### Heading 3
Paragraph text {x3}
> Blockquote text
> {cite="المصدر" x=2}
- Bullet list
1. Ordered list
أ. Abjad list
:::poem{cite="الشاعر"}
صدر | عجز
صدر | عجز {x3}
***
صدر | شطر ثاني | شطر ثالث
:::Escaping
\]inside:name[content]— literal]\"inside{key="value"}— literal"\{at start of blockquote line — literal, not metadata
Known Limitations
^^inside propheticSpeech mark content is not safe}inside inline-repeat mark content is not safe\]in directive content loses the backslash- Blockquote line exactly matching
{key="value"}is consumed as metadata
These do not affect Arabic Islamic content.
Schema Types
import type { QalamDoc, QalamDocResolved, QalamBlock, QalamInline } from "@siladev/qalam";Standalone Markdown Manager
import { createMarkdownManager } from "@siladev/qalam";
const manager = createMarkdownManager();
const json = manager.parse(markdown);
const md = manager.serialize(json);