npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/qalam

Editor

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 threshold are 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 bundled HafsSmart font.
  • "uthmani" — canonical Uthmani script with ۝ + Arabic-Indic digit aya markers. Renders properly in Amiri Quran (open-source, available on Google Fonts), KFGQPC Uthmanic Script, or Scheherazade 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/7

Use 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 Roman for Word/LibreOffice compatibility; pass bodyFont to target an installed font such as Amiri.
  • DOCX footnotes are native Word footnotes, so Word/LibreOffice place them per page.
  • linkResolver is applied to HTML and DOCX hyperlinks.
  • pageBreak exports to CSS page breaks and DOCX page breaks.
  • HTML/PDF footnotes are 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);