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

@topik/content

v0.1.0-alpha.6

Published

Declarative Topik content model, Markdown format, validation and rendering

Readme

@topik/content

Portable Markdown with declarative components, variables, and conditions. This package provides an mdast authoring tree, validation, lossless source operations, and a framework-independent render tree. React rendering lives in @topik/content-react; applications own editing, storage, and synchronization.

The package is ESM and requires Node.js 22.12 or later. Install it with your package manager:

npm install @topik/content

Validate, format, and render

compileTopikContent validates source and produces a reader tree in one pipeline. It accepts canonical compiler-generated asset references as well as authored content. Parsing alone checks neither URL nor asset admission policies. Formatting validates input and output and checks that serialization preserves the authored content. Failures retain the exact input.

import { formatTopikContent, compileTopikContent } from "@topik/content";

const source = '# Welcome\n\n{% callout variant="tip" %}\nKeep a **backup**.\n{% /callout %}';
const formatted = formatTopikContent(source);
if (!formatted.ok) throw new Error(JSON.stringify(formatted.diagnostics));

const compiled = compileTopikContent(formatted.formatted);
if (!compiled.ok) throw new Error(JSON.stringify(compiled.diagnostics));
const tree = compiled.tree;
// Store authored source; pass the derived tree to your renderer.
if (tree === null) throw new Error("Expected rendered content");

parseDocument and validateDocument check tree structure and component rules. writeDocument serializes a supported authoring tree. ASTs must contain plain data with independent node objects: cycles, shared nodes, accessors, and executable values are refused. Use validateTopikContent for source admission, including navigation and asset policies.

sameDocumentMeaning(left, right) compares already-admitted authoring trees. It ignores source positions, cosmetic spelling and derived metadata while retaining explicit heading IDs, reference bindings, ordered content and every conditional branch. It does not validate arbitrary trees or treat application editor defaults as authored defaults.

Markdown and components

Supported Markdown includes headings, emphasis, strong and strikethrough marks, code, lists and task lists, blockquotes, hard breaks, links, images, reference definitions, tables, and leading YAML frontmatter. Frontmatter is retained but not rendered. Raw HTML and footnotes are refused. URLs and email addresses remain text unless expressed as explicit Markdown links or angle-bracket autolinks.

Portable document destinations use ref://wiki-page/<name>, ref://guide/<name>, or ref://course-page/<name>, where the name is the target resource identity. parseTopikResourceReference and serializeTopikResourceReference retain its query and fragment separately from publishing routes. Renderers must resolve these identities to browser URLs; see the resource-link reference.

Block tags occupy their own lines; inline tags appear inside prose. Properties are literal strings, numbers, and booleans; selected built-in text properties also accept explicit templates. Unsupported syntax is diagnosed instead of silently discarded.

| Component | Content and principal properties | | ------------------------------------------- | --------------------------------------------------------------------------------------------------- | | callout | Block content; optional variant and title | | accordion | Block content; required title, optional open | | cardGrid, card | Cards; grid columns from 1 to 4; card requires title and accepts href, icon | | tabs, tab | One or more tabs, each with a required title | | steps, step | One or more steps; optional step title | | codeGroup, codeTab | One or more code tabs; each requires title and fenced code | | figure | Self-closing; required src and alt, optional darkSrc and caption | | math, mathInline | Self-closing block/inline formula with required content | | badge | Inline content; optional variant | | underline (alias u) | Inline content | | quiz, question, choice, explanation | Questions with at least two choices; type selects single/multiple choice, correct marks answers |

Math content preserves line breaks and tabs using \n, \r and \t escapes. Other attributes continue to refuse control characters. These escapes remain literal formula data; they do not add Markdown blocks or inline nodes.

The exported components registry contains complete property, placement, and child constraints. Defaults are applied when reading or rendering; absent authored properties remain absent when writing.

Fenced code accepts presentation attributes such as filename="Client.java", lines, highlight="2" and collapse="3-4,8-9". Selections use closed physical-row ranges and normalize to sorted, merged intervals. Each collapsed interval has an independent inline control; Copy always retains the full exact code, and every row remains visible before hydration or without JavaScript. See the code presentation reference for options, limits and source-preservation rules.

Variables and conditions

Evaluation selects branches and inserts variable values as literal text. It does not execute JavaScript or interpret interpolated Markdown. Save the authored document to retain variables and every branch.

import { evaluateDocument, parseDocument, writeDocument } from "@topik/content";

const source = [
  '{% if equals($student.role, "teacher") %}',
  "Hello {% $student.name %}.",
  "{% else /%}",
  "Student instructions.",
  "{% /if %}",
].join("\n");
const parsed = parseDocument(source);
if (!parsed.ok) throw new Error(JSON.stringify(parsed.diagnostics));

const evaluated = evaluateDocument(parsed.document, {
  student: { role: "teacher", name: "Ada" },
});
if (writeDocument(evaluated) !== "Hello Ada.\n") throw new Error("Unexpected evaluation");

Expressions support scalar literals, variable paths, equals, and, or, and not. Only false and null are false; and and or short-circuit. Missing variables in evaluated paths throw ContentEvaluationError. Conditions control presentation and do not enforce authorization.

Null and empty strings insert no text. Evaluation omits empty paragraphs, empty marks, and empty components that require inline children. It also removes terminal hard breaks exposed by those omissions so the evaluated document remains writable.

evaluateDocument verifies that its result can be written faithfully. If evaluation creates an unrepresentable structure, such as an empty task item or adjacent strikethrough spans, it throws ContentEvaluationError and leaves the authored document untouched. Rendering through transformTopikContent does not require a Markdown representation and retains those structures in its derived render tree.

Rendering assigns heading IDs using all authored headings before selecting branches or interpolating text, matching analyzeTopikContent. Hidden headings still reserve their IDs. Use explicit IDs, such as ## Hello {% $name %} {% #hello %}, when a stable descriptive anchor is needed for a variable heading.

Text and code templates

Use t"..." to interpolate title on callout, accordion, card, tab, step, and codeTab, or alt/caption on figure. Ordinary quoted properties stay literal. Empty and literal-only templates retain their template identity; an evaluated empty template remains a present empty value.

Wrap exactly one fenced code block in {% template code %} and {% /template code %} on their own block lines. The markers and fence must share one logical parent, including a codeTab body. Empty payloads are valid; nested, missing, crossing, multi-fence, or indented-code scopes are refused. Language and metadata remain literal. Templated mermaid fences are refused before diagram dispatch; unknown languages remain ordinary code. Inline code and unwrapped code blocks stay literal.

import { compileTopikContent, formatTopikContent } from "@topik/content";

const source = [
  '{% callout title=t"Install {% $package.name %}" %}',
  "{% template code %}",
  "~~~sh",
  "npm install {% $package.name %}@{% $package.version %}",
  "~~~",
  "{% /template code %}",
  "{% /callout %}",
].join("\n");
const formatted = formatTopikContent(source);
if (!formatted.ok) throw new Error(JSON.stringify(formatted.diagnostics));
const compiled = compileTopikContent(source, {
  config: { variables: { package: { name: "example-kit", version: "1.2.3" } } },
});
if (!compiled.ok) throw new Error(JSON.stringify(compiled.diagnostics));
// Store source or formatted.formatted; compiled.tree contains reader values.
if (compiled.source !== source) throw new Error("Authoring source changed");

Inside templates, {% $path %} inserts a scalar and {%% writes a literal {% opener. Scanning is single-pass: inserted values and escaped literals are never interpreted as new tokens. Code backslashes are unchanged; attribute templates retain ordinary quoted-string escapes. Dotted paths use identifier segments; expressions, functions, defaults, and bracket paths are unsupported.

Strings, finite numbers, and booleans become raw text; null and empty strings become empty text. Missing/incompatible values and injected C0/DEL controls, including newlines and tabs, fail visibly. Each substitution is single-line. Values create no Markdown/HTML structure and receive normal renderer escaping; authors supply any shell/JSON/language quoting. Inactive branches validate source without resolving their variables.

URLs, image sources, icons, math, enums, and custom component properties remain literal-only. Formatting preserves unevaluated templates and every branch; evaluation produces a separate derived result. See the template reference for complete scope, escape, diagnostic, and source-storage rules.

Custom components

Add declarative schemas through config.components. Built-in schemas cannot be overridden; if and else are reserved for conditions and cannot be component names. Custom runtime functions, partials, and executable property values are unsupported. Supply the corresponding render component in your application. Use ComponentSchema to type a reusable custom schema; ComponentDefinition additionally describes the built-in validators used by the runtime registry.

import {
  parseTopikContent,
  transformTopikContent,
  validateTopikContent,
  type TopikContentConfig,
} from "@topik/content";

const config: TopikContentConfig = {
  components: {
    notice: {
      kind: "block",
      render: "Notice",
      attributes: { title: { type: "string", required: true } },
      children: "blocks",
    },
  },
};
const source = '{% notice title="Remember" %}\nKeep a backup.\n{% /notice %}';
if (!validateTopikContent(source, { config }).valid) throw new Error("Invalid notice");
transformTopikContent(parseTopikContent(source, { config }), config);

Asset rewriting

Rewriting checks all authored branches and retains the original source on failure. Callbacks run after input admission; exceptions propagate. Keep callbacks free of side effects because a later replacement can invalidate the entire operation.

import { extractTopikAssetOccurrences, rewriteTopikAssetOccurrences } from "@topik/content";

const result = rewriteTopikAssetOccurrences("![Diagram](old.png)", (occurrence) =>
  occurrence.reference === "old.png" ? "new.png" : undefined,
);
if (!result.ok) throw new Error(JSON.stringify(result.diagnostics));
if (extractTopikAssetOccurrences(result.content)[0]?.reference !== "new.png") {
  throw new Error("Asset replacement was not preserved");
}

Images and figure sources accept canonical relative paths or credential-free HTTPS URLs. Compiler-generated asset: references are accepted in rewritten output; source admission requires allowCompiledAssetReferences: true to accept them. Generic download links require explicit candidate selection or proven file paths. Custom asset attributes share validation policies but do not extend the compiler's closed asset-occurrence protocol.

Further documentation

The schema version is 0.2.2 and FORMAT_VERSION is 1. This package is alpha software. Review canonical output when upgrading and keep compiler and renderer versions aligned. The package verifier type-checks and executes every TypeScript example above against the published-package layout.