@topik/content 0.0.0-stage → 0.1.0-alpha.7
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 +21 -0
- package/README.md +267 -2
- package/dist/index.d.mts +723 -0
- package/dist/index.mjs +4472 -0
- package/package.json +62 -3
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
|
-
#
|
|
1
|
+
# @topik/content
|
|
2
2
|
|
|
3
|
-
|
|
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("", (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.
|