@topik/content 0.0.0-stage → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Topik Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,268 @@
1
- # Temporary Holding Version
1
+ # @topik/content
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Portable Markdown with declarative components, variables, and conditions. This
4
+ package provides an mdast authoring tree, validation, lossless source operations,
5
+ and a framework-independent render tree. React rendering lives in
6
+ `@topik/content-react`; applications own editing, storage, and synchronization.
7
+
8
+ The package is ESM and requires Node.js 22.12 or later. Install it with your
9
+ package manager:
10
+
11
+ ```sh
12
+ npm install @topik/content
13
+ ```
14
+
15
+ ## Validate, format, and render
16
+
17
+ `compileTopikContent` validates source and produces a reader tree in one pipeline.
18
+ It accepts canonical compiler-generated asset references as well as authored
19
+ content. Parsing alone checks neither URL nor asset admission policies. Formatting
20
+ validates input and output and checks that serialization preserves the authored
21
+ content. Failures retain the exact input.
22
+
23
+ ```ts
24
+ import { formatTopikContent, compileTopikContent } from "@topik/content";
25
+
26
+ const source = '# Welcome\n\n{% callout variant="tip" %}\nKeep a **backup**.\n{% /callout %}';
27
+ const formatted = formatTopikContent(source);
28
+ if (!formatted.ok) throw new Error(JSON.stringify(formatted.diagnostics));
29
+
30
+ const compiled = compileTopikContent(formatted.formatted);
31
+ if (!compiled.ok) throw new Error(JSON.stringify(compiled.diagnostics));
32
+ const tree = compiled.tree;
33
+ // Store authored source; pass the derived tree to your renderer.
34
+ if (tree === null) throw new Error("Expected rendered content");
35
+ ```
36
+
37
+ `parseDocument` and `validateDocument` check tree structure and component rules.
38
+ `writeDocument` serializes a supported authoring tree. ASTs must contain plain
39
+ data with independent node objects: cycles, shared nodes, accessors, and executable
40
+ values are refused. Use `validateTopikContent` for source admission, including
41
+ navigation and asset policies.
42
+
43
+ `sameDocumentMeaning(left, right)` compares already-admitted authoring trees. It
44
+ ignores source positions, cosmetic spelling and derived metadata while retaining
45
+ explicit heading IDs, reference bindings, ordered content and every conditional
46
+ branch. It does not validate arbitrary trees or treat application editor defaults
47
+ as authored defaults.
48
+
49
+ ## Markdown and components
50
+
51
+ Supported Markdown includes headings, emphasis, strong and strikethrough marks,
52
+ code, lists and task lists, blockquotes, hard breaks, links, images, reference
53
+ definitions, tables, and leading YAML frontmatter. Frontmatter is retained but
54
+ not rendered. Raw HTML and footnotes are refused. URLs and email addresses remain
55
+ text unless expressed as explicit Markdown links or angle-bracket autolinks.
56
+
57
+ Portable document destinations use `ref://wiki-page/<name>`, `ref://guide/<name>`,
58
+ or `ref://course-page/<name>`, where the name is the target resource identity.
59
+ `parseTopikResourceReference` and `serializeTopikResourceReference` retain its
60
+ query and fragment separately from publishing routes. Renderers must resolve
61
+ these identities to browser URLs; see the
62
+ [resource-link reference](https://github.com/subtopik/topik/blob/main/docs/resources/navigation.md#links-between-pages).
63
+
64
+ Block tags occupy their own lines; inline tags appear inside prose. Properties
65
+ are literal strings, numbers, and booleans; selected built-in text properties also
66
+ accept explicit templates. Unsupported syntax is diagnosed instead of silently
67
+ discarded.
68
+
69
+ | Component | Content and principal properties |
70
+ | ------------------------------------------- | --------------------------------------------------------------------------------------------------- |
71
+ | `callout` | Block content; optional `variant` and `title` |
72
+ | `accordion` | Block content; required `title`, optional `open` |
73
+ | `cardGrid`, `card` | Cards; grid `columns` from 1 to 4; card requires `title` and accepts `href`, `icon` |
74
+ | `tabs`, `tab` | One or more tabs, each with a required `title` |
75
+ | `steps`, `step` | One or more steps; optional step `title` |
76
+ | `codeGroup`, `codeTab` | One or more code tabs; each requires `title` and fenced code |
77
+ | `figure` | Self-closing; required `src` and `alt`, optional `darkSrc` and `caption` |
78
+ | `math`, `mathInline` | Self-closing block/inline formula with required `content` |
79
+ | `badge` | Inline content; optional `variant` |
80
+ | `underline` (alias `u`) | Inline content |
81
+ | `quiz`, `question`, `choice`, `explanation` | Questions with at least two choices; `type` selects single/multiple choice, `correct` marks answers |
82
+
83
+ Math `content` preserves line breaks and tabs using `\n`, `\r` and `\t`
84
+ escapes. Other attributes continue to refuse control characters. These escapes
85
+ remain literal formula data; they do not add Markdown blocks or inline nodes.
86
+
87
+ The exported `components` registry contains complete property, placement, and
88
+ child constraints. Defaults are applied when reading or rendering; absent authored
89
+ properties remain absent when writing.
90
+
91
+ Fenced code accepts presentation attributes such as `filename="Client.java"`,
92
+ `lines`, `highlight="2"` and `collapse="3-4,8-9"`. Selections use closed physical-row
93
+ ranges and normalize to sorted, merged intervals. Each collapsed interval has an
94
+ independent inline control; Copy always retains the full exact code, and every row
95
+ remains visible before hydration or without JavaScript. See the
96
+ [code presentation reference](https://github.com/subtopik/topik/blob/main/docs/content/code-presentation.md)
97
+ for options, limits and source-preservation rules.
98
+
99
+ ## Variables and conditions
100
+
101
+ Evaluation selects branches and inserts variable values as literal text. It does
102
+ not execute JavaScript or interpret interpolated Markdown. Save the authored
103
+ document to retain variables and every branch.
104
+
105
+ ```ts
106
+ import { evaluateDocument, parseDocument, writeDocument } from "@topik/content";
107
+
108
+ const source = [
109
+ '{% if equals($student.role, "teacher") %}',
110
+ "Hello {% $student.name %}.",
111
+ "{% else /%}",
112
+ "Student instructions.",
113
+ "{% /if %}",
114
+ ].join("\n");
115
+ const parsed = parseDocument(source);
116
+ if (!parsed.ok) throw new Error(JSON.stringify(parsed.diagnostics));
117
+
118
+ const evaluated = evaluateDocument(parsed.document, {
119
+ student: { role: "teacher", name: "Ada" },
120
+ });
121
+ if (writeDocument(evaluated) !== "Hello Ada.\n") throw new Error("Unexpected evaluation");
122
+ ```
123
+
124
+ Expressions support scalar literals, variable paths, `equals`, `and`, `or`, and
125
+ `not`. Only `false` and `null` are false; `and` and `or` short-circuit. Missing
126
+ variables in evaluated paths throw `ContentEvaluationError`. Conditions control
127
+ presentation and do not enforce authorization.
128
+
129
+ Null and empty strings insert no text. Evaluation omits empty paragraphs, empty
130
+ marks, and empty components that require inline children. It also removes terminal
131
+ hard breaks exposed by those omissions so the evaluated document remains writable.
132
+
133
+ `evaluateDocument` verifies that its result can be written faithfully. If evaluation
134
+ creates an unrepresentable structure, such as an empty task item or adjacent
135
+ strikethrough spans, it throws `ContentEvaluationError` and leaves the authored
136
+ document untouched. Rendering through `transformTopikContent` does not require a
137
+ Markdown representation and retains those structures in its derived render tree.
138
+
139
+ Rendering assigns heading IDs using all authored headings before selecting
140
+ branches or interpolating text, matching `analyzeTopikContent`. Hidden headings
141
+ still reserve their IDs. Use explicit IDs, such as `## Hello {% $name %} {% #hello %}`,
142
+ when a stable descriptive anchor is needed for a variable heading.
143
+
144
+ ## Text and code templates
145
+
146
+ Use `t"..."` to interpolate `title` on `callout`, `accordion`, `card`, `tab`,
147
+ `step`, and `codeTab`, or `alt`/`caption` on `figure`. Ordinary quoted properties
148
+ stay literal. Empty and literal-only templates retain their template identity;
149
+ an evaluated empty template remains a present empty value.
150
+
151
+ Wrap exactly one fenced code block in `{% template code %}` and
152
+ `{% /template code %}` on their own block lines. The markers and fence must share
153
+ one logical parent, including a `codeTab` body. Empty payloads are valid; nested,
154
+ missing, crossing, multi-fence, or indented-code scopes are refused. Language and
155
+ metadata remain literal. Templated `mermaid` fences are refused before diagram
156
+ dispatch; unknown languages remain ordinary code. Inline code and unwrapped code
157
+ blocks stay literal.
158
+
159
+ ```ts
160
+ import { compileTopikContent, formatTopikContent } from "@topik/content";
161
+
162
+ const source = [
163
+ '{% callout title=t"Install {% $package.name %}" %}',
164
+ "{% template code %}",
165
+ "~~~sh",
166
+ "npm install {% $package.name %}@{% $package.version %}",
167
+ "~~~",
168
+ "{% /template code %}",
169
+ "{% /callout %}",
170
+ ].join("\n");
171
+ const formatted = formatTopikContent(source);
172
+ if (!formatted.ok) throw new Error(JSON.stringify(formatted.diagnostics));
173
+ const compiled = compileTopikContent(source, {
174
+ config: { variables: { package: { name: "example-kit", version: "1.2.3" } } },
175
+ });
176
+ if (!compiled.ok) throw new Error(JSON.stringify(compiled.diagnostics));
177
+ // Store source or formatted.formatted; compiled.tree contains reader values.
178
+ if (compiled.source !== source) throw new Error("Authoring source changed");
179
+ ```
180
+
181
+ Inside templates, `{% $path %}` inserts a scalar and `{%%` writes a literal `{%`
182
+ opener. Scanning is single-pass: inserted values and escaped literals are never
183
+ interpreted as new tokens. Code backslashes are unchanged; attribute templates
184
+ retain ordinary quoted-string escapes. Dotted paths use identifier segments;
185
+ expressions, functions, defaults, and bracket paths are unsupported.
186
+
187
+ Strings, finite numbers, and booleans become raw text; null and empty strings
188
+ become empty text. Missing/incompatible values and injected C0/DEL controls,
189
+ including newlines and tabs, fail visibly. Each substitution is single-line.
190
+ Values create no Markdown/HTML structure and receive normal renderer escaping;
191
+ authors supply any shell/JSON/language quoting. Inactive branches validate source
192
+ without resolving their variables.
193
+
194
+ URLs, image sources, icons, math, enums, and custom component properties remain
195
+ literal-only. Formatting preserves unevaluated templates and every branch;
196
+ evaluation produces a separate derived result. See the [template
197
+ reference](https://github.com/subtopik/topik/blob/main/docs/content/templates.md)
198
+ for complete scope, escape, diagnostic, and source-storage rules.
199
+
200
+ ## Custom components
201
+
202
+ Add declarative schemas through `config.components`. Built-in schemas cannot be
203
+ overridden; `if` and `else` are reserved for conditions and cannot be component names.
204
+ Custom runtime functions, partials, and executable property values
205
+ are unsupported. Supply the corresponding render component in your application.
206
+ Use `ComponentSchema` to type a reusable custom schema; `ComponentDefinition`
207
+ additionally describes the built-in validators used by the runtime registry.
208
+
209
+ ```ts
210
+ import {
211
+ parseTopikContent,
212
+ transformTopikContent,
213
+ validateTopikContent,
214
+ type TopikContentConfig,
215
+ } from "@topik/content";
216
+
217
+ const config: TopikContentConfig = {
218
+ components: {
219
+ notice: {
220
+ kind: "block",
221
+ render: "Notice",
222
+ attributes: { title: { type: "string", required: true } },
223
+ children: "blocks",
224
+ },
225
+ },
226
+ };
227
+ const source = '{% notice title="Remember" %}\nKeep a backup.\n{% /notice %}';
228
+ if (!validateTopikContent(source, { config }).valid) throw new Error("Invalid notice");
229
+ transformTopikContent(parseTopikContent(source, { config }), config);
230
+ ```
231
+
232
+ ## Asset rewriting
233
+
234
+ Rewriting checks all authored branches and retains the original source on failure.
235
+ Callbacks run after input admission; exceptions propagate. Keep callbacks free
236
+ of side effects because a later replacement can invalidate the entire operation.
237
+
238
+ ```ts
239
+ import { extractTopikAssetOccurrences, rewriteTopikAssetOccurrences } from "@topik/content";
240
+
241
+ const result = rewriteTopikAssetOccurrences("![Diagram](old.png)", (occurrence) =>
242
+ occurrence.reference === "old.png" ? "new.png" : undefined,
243
+ );
244
+ if (!result.ok) throw new Error(JSON.stringify(result.diagnostics));
245
+ if (extractTopikAssetOccurrences(result.content)[0]?.reference !== "new.png") {
246
+ throw new Error("Asset replacement was not preserved");
247
+ }
248
+ ```
249
+
250
+ Images and figure sources accept canonical relative paths or credential-free
251
+ HTTPS URLs. Compiler-generated `asset:` references are accepted in rewritten
252
+ output; source admission requires `allowCompiledAssetReferences: true` to accept
253
+ them. Generic download links require explicit candidate selection or proven file
254
+ paths. Custom asset attributes share validation policies but do not extend the
255
+ compiler's closed asset-occurrence protocol.
256
+
257
+ ## Further documentation
258
+
259
+ - [Format, normalization, and reference scopes](https://github.com/subtopik/topik/blob/main/docs/content/content-design.md)
260
+ - [Text and code templates](https://github.com/subtopik/topik/blob/main/docs/content/templates.md)
261
+ - [Code presentation](https://github.com/subtopik/topik/blob/main/docs/content/code-presentation.md)
262
+ - [Explicit links and plain URLs](https://github.com/subtopik/topik/blob/main/docs/content/autolinks.md)
263
+ - [Rendering and migration from content-schema](https://github.com/subtopik/topik/blob/main/docs/content/rendering.md)
264
+
265
+ The schema version is `0.2.2` and `FORMAT_VERSION` is `1`. This package is alpha
266
+ software. Review canonical output when upgrading and keep compiler and renderer
267
+ versions aligned. The package verifier type-checks and executes every TypeScript
268
+ example above against the published-package layout.