@topik/remark-tags 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 +21 -0
- package/README.md +208 -2
- package/dist/index.d.mts +142 -0
- package/dist/index.mjs +971 -0
- package/package.json +64 -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,209 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @topik/remark-tags
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Declare inline and block tags in Markdown using `{% name %}` syntax. This package
|
|
4
|
+
provides a remark plugin and the underlying micromark/mdast extensions. It parses
|
|
5
|
+
and writes syntax; component schemas, expression evaluation, and rendering belong
|
|
6
|
+
to the application.
|
|
7
|
+
|
|
8
|
+
## Install and use
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @topik/remark-tags unified remark-parse remark-stringify
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { unified } from "unified";
|
|
16
|
+
import remarkParse from "remark-parse";
|
|
17
|
+
import remarkStringify from "remark-stringify";
|
|
18
|
+
import remarkTags from "@topik/remark-tags";
|
|
19
|
+
|
|
20
|
+
const tags = {
|
|
21
|
+
badge: { kind: "inline" },
|
|
22
|
+
panel: { kind: "block" },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const processor = unified().use(remarkParse).use(remarkTags, tags).use(remarkStringify);
|
|
26
|
+
|
|
27
|
+
const file = processor.processSync(`{% panel title="Release" %}
|
|
28
|
+
Available {% badge %}**today**{% /badge %}.
|
|
29
|
+
{% /panel %}`);
|
|
30
|
+
|
|
31
|
+
console.log(String(file));
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The default export and named `remarkTags` export refer to the same plugin. This is
|
|
35
|
+
an ESM package requiring Node.js 22.12 or later.
|
|
36
|
+
|
|
37
|
+
Contributor tooling uses the workspace's `.node-version` and requires
|
|
38
|
+
`^22.18.0 || ^24.11.0 || >=26.0.0` for Vite+ 1.0 and Vitest 5. CI verifies the
|
|
39
|
+
built library on Node.js 22.12 separately from the test runner.
|
|
40
|
+
|
|
41
|
+
## Syntax
|
|
42
|
+
|
|
43
|
+
- Declare each tag's placement as `inline` or `block`. Unknown names are errors.
|
|
44
|
+
- Block opening and closing tags occupy their own lines, including inside lists
|
|
45
|
+
and blockquotes. An inline tag on its own line stays in the surrounding paragraph.
|
|
46
|
+
- Use `{% badge /%}` for a self-closing tag or
|
|
47
|
+
`{% badge %}Markdown children{% /badge %}` for a paired tag.
|
|
48
|
+
- Names start with a lowercase ASCII letter, followed by letters, digits, or
|
|
49
|
+
hyphens. Names are case-sensitive; camelCase is supported.
|
|
50
|
+
- Attributes use `key=value` with double-quoted strings, finite numbers, or
|
|
51
|
+
booleans: `title="Preview" columns=2 open=true`. Strings support `\\`, `\"`,
|
|
52
|
+
`\|`, `\[`, and `\]` escapes. Empty strings are preserved; control characters
|
|
53
|
+
are refused. Applications can explicitly admit `\n`, `\r` and `\t`
|
|
54
|
+
for named ordinary string attributes with
|
|
55
|
+
`escapedWhitespace: { component: ["attribute"] }`;
|
|
56
|
+
raw control characters and all other control escapes remain refused.
|
|
57
|
+
The writer escapes brackets in attributes inside Markdown labels,
|
|
58
|
+
including native directive labels, to preserve their surrounding structure.
|
|
59
|
+
- Tag-looking text in code, escaped openers, and raw HTML blocks remains literal.
|
|
60
|
+
- Tags nest within Markdown boundaries. Opening inside emphasis and closing
|
|
61
|
+
outside it, or opening an inline tag in one paragraph and closing it in another,
|
|
62
|
+
is an error.
|
|
63
|
+
|
|
64
|
+
This is Markdoc-style syntax, not a complete Markdoc implementation. Standard
|
|
65
|
+
remark directives (`:name`, `::name`, `:::name`) remain independent. GFM and other
|
|
66
|
+
Markdown features are enabled through their own plugins.
|
|
67
|
+
|
|
68
|
+
The writer sorts attribute names by code unit, escapes values, and normalizes
|
|
69
|
+
spacing. It preserves an empty block container as a container; empty inline tags
|
|
70
|
+
normalize to self-closing tags. Null and undefined attribute values in constructed
|
|
71
|
+
trees are omitted. Caller-built attribute maps must be plain objects (including
|
|
72
|
+
null-prototype objects) with enumerable own data properties; accessors and custom
|
|
73
|
+
prototypes are refused without invoking their getters. Formatting preserves
|
|
74
|
+
supported tree meaning, not source bytes.
|
|
75
|
+
HTML blocks remain separated from following tag markers by a blank line. Blank
|
|
76
|
+
lines inside a block tag do not make its enclosing list item loose; blank lines
|
|
77
|
+
between the item's direct block children still do.
|
|
78
|
+
Literal tag openers are escaped without changing URL or email autolinks. When
|
|
79
|
+
emphasis or strong marks nest across inline tags, their delimiters alternate to
|
|
80
|
+
keep the tag boundaries intact; the surrounding writer's preferences are restored
|
|
81
|
+
after each tag.
|
|
82
|
+
|
|
83
|
+
## AST and extensions
|
|
84
|
+
|
|
85
|
+
| Node | Meaning |
|
|
86
|
+
| ----------------- | --------------------------------------------------------------------- |
|
|
87
|
+
| `tagText` | Inline tag with phrasing children; self-closing tags have no children |
|
|
88
|
+
| `tagLeaf` | Self-closing block tag with no children |
|
|
89
|
+
| `tagContainer` | Paired block tag with block/definition children |
|
|
90
|
+
| `tagVariable` | Opt-in variable reference with a string `path` |
|
|
91
|
+
| `tagConditional` | Opt-in conditional containing one or two `tagBranch` nodes |
|
|
92
|
+
| `tagBranch` | Flow children with a condition string, or `null` for the else branch |
|
|
93
|
+
| `tagCodeTemplate` | Opt-in fenced code with literal language/metadata and a text template |
|
|
94
|
+
|
|
95
|
+
Component nodes contain `name` and `attributes`. Parsed nodes retain source
|
|
96
|
+
positions. Exported node interfaces augment mdast's node maps for TypeScript users.
|
|
97
|
+
Branch positions span from the `if` or `else` marker through the last child,
|
|
98
|
+
excluding the following marker and trailing blank lines. Empty branches cover
|
|
99
|
+
only their opening marker.
|
|
100
|
+
|
|
101
|
+
Use the same declarations and options with all three extensions when working
|
|
102
|
+
directly with mdast:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
106
|
+
import { toMarkdown } from "mdast-util-to-markdown";
|
|
107
|
+
import { tagSyntax, tagFromMarkdown, tagToMarkdown } from "@topik/remark-tags";
|
|
108
|
+
|
|
109
|
+
const tags = { badge: { kind: "inline" } };
|
|
110
|
+
const tree = fromMarkdown('{% badge label="Preview" /%}', {
|
|
111
|
+
extensions: [tagSyntax(tags)],
|
|
112
|
+
mdastExtensions: [tagFromMarkdown(tags)],
|
|
113
|
+
});
|
|
114
|
+
const source = toMarkdown(tree, { extensions: [tagToMarkdown(tags)] });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Optional expression syntax
|
|
118
|
+
|
|
119
|
+
Pass `{ expressions: true }` as the plugin's third `.use()` argument, or as the
|
|
120
|
+
second argument to each extension, to enable:
|
|
121
|
+
|
|
122
|
+
```md
|
|
123
|
+
Hello {% $student.name %}.
|
|
124
|
+
|
|
125
|
+
{% if equals($student.role, "mentor") %}
|
|
126
|
+
Mentor instructions.
|
|
127
|
+
{% else /%}
|
|
128
|
+
Student instructions.
|
|
129
|
+
{% /if %}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Variable paths are dot-separated identifiers. Conditions are preserved as opaque
|
|
133
|
+
source strings with balanced quotes and safe delimiters; this package neither
|
|
134
|
+
evaluates them nor validates an expression language. Applications must supply that
|
|
135
|
+
behavior. `if` and `else` are reserved declaration names when expressions are
|
|
136
|
+
enabled. Conditions are block-only, with at most one else branch.
|
|
137
|
+
|
|
138
|
+
### Explicit text templates
|
|
139
|
+
|
|
140
|
+
The same option enables `t"..."` attribute values and paired code-template scopes:
|
|
141
|
+
|
|
142
|
+
````md
|
|
143
|
+
{% panel title=t"Install {% $package.name %}" %}
|
|
144
|
+
{% template code %}
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
npm install {% $package.name %}
|
|
148
|
+
echo '{%% $literal.example %}'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
{% /template code %}
|
|
152
|
+
{% /panel %}
|
|
153
|
+
````
|
|
154
|
+
|
|
155
|
+
Ordinary quoted attributes and ordinary code blocks retain their literal meaning.
|
|
156
|
+
Inside an explicit template, `{% $path %}` is a reference and `{%%` produces the
|
|
157
|
+
literal opener `{%`. Escape processing runs once: `{%%%` produces `{%%`. Code
|
|
158
|
+
backslashes stay literal; attribute templates use the usual quoted-string escapes
|
|
159
|
+
before scanning their references. Attribute template literals cannot contain
|
|
160
|
+
controls or line breaks. Code literals cannot contain NUL or carriage returns;
|
|
161
|
+
source line endings normalize to LF. Template paths use dot-separated ASCII
|
|
162
|
+
identifiers and refuse `__proto__`, `prototype`, and `constructor` segments.
|
|
163
|
+
|
|
164
|
+
Code scope markers occupy their own block lines and contain exactly one closed
|
|
165
|
+
fenced block in the same Markdown parent. They can appear inside lists,
|
|
166
|
+
blockquotes, declared block tags, and conditional branches. Extra content,
|
|
167
|
+
nested scopes, indented code, missing closers, and interpreted `mermaid` code are
|
|
168
|
+
refused. Language and metadata stay literal. The special two-word markers leave
|
|
169
|
+
custom tags named `template` and `codeTemplate` available.
|
|
170
|
+
|
|
171
|
+
Template attributes contain an exported `TagTextTemplate` value:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
{
|
|
175
|
+
type: "topikTextTemplate",
|
|
176
|
+
segments: [
|
|
177
|
+
{ type: "literal", value: "Install " },
|
|
178
|
+
{ type: "variable", path: ["package", "name"] },
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`tagCodeTemplate` is a leaf with `lang`, `meta`, and `template`; it has no ordinary
|
|
184
|
+
`value` or children. Empty and literal-only templates keep their template identity.
|
|
185
|
+
The writer merges adjacent literal segments, escapes literal openers, and chooses
|
|
186
|
+
a fence that preserves the payload. It preserves decoded fence metadata through
|
|
187
|
+
character references when needed, including tabs and leading spaces. Applications
|
|
188
|
+
choose eligible attribute slots, resolve references, and render the resulting text;
|
|
189
|
+
this package preserves the authored structure without evaluating it.
|
|
190
|
+
|
|
191
|
+
## Diagnostics and limits
|
|
192
|
+
|
|
193
|
+
Malformed source throws `TagSyntaxError`. Its `diagnostics` array contains a
|
|
194
|
+
`message` and, when available, one-based `line` and `column`. Structural limit
|
|
195
|
+
errors also have `id: "tag-content-limit"`. Invalid declarations and unsupported
|
|
196
|
+
writer inputs throw ordinary errors.
|
|
197
|
+
|
|
198
|
+
Template syntax uses stable `topik-template-syntax`, `topik-variable-path`,
|
|
199
|
+
`topik-template-control`, `topik-code-template-structure`, and
|
|
200
|
+
`topik-code-template-language` diagnostic IDs.
|
|
201
|
+
|
|
202
|
+
`TAG_LIMITS` bounds both the incoming Markdown tree and the converted tag tree to
|
|
203
|
+
depth 128 and 50,000 nodes, counting template objects and segments in the converted
|
|
204
|
+
tree. It also bounds each template's decoded input payload and generated source
|
|
205
|
+
value to 1,000,000 UTF-16 units. Quoted template output counts the `t` prefix,
|
|
206
|
+
quotes, and surrounding Markdown escape rules; code output counts generated
|
|
207
|
+
fences and scope markers.
|
|
208
|
+
These limits do not bound an entire source document. Host applications remain
|
|
209
|
+
responsible for their own input limits and rendering policy.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { BlockContent, DefinitionContent, Node, Parent, PhrasingContent, Root } from "mdast";
|
|
2
|
+
import { Extension } from "mdast-util-from-markdown";
|
|
3
|
+
import { Options } from "mdast-util-to-markdown";
|
|
4
|
+
import { Extension as Extension$1 } from "micromark-util-types";
|
|
5
|
+
import { Plugin } from "unified";
|
|
6
|
+
//#region src/types.d.ts
|
|
7
|
+
export type TagDeclarations = Record<string, {
|
|
8
|
+
kind: "inline" | "block";
|
|
9
|
+
}>;
|
|
10
|
+
export interface TagTextTemplate {
|
|
11
|
+
type: "topikTextTemplate";
|
|
12
|
+
segments: Array<{
|
|
13
|
+
type: "literal";
|
|
14
|
+
value: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: "variable";
|
|
17
|
+
path: string[];
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export type TagAttributeValue = string | number | boolean | TagTextTemplate;
|
|
21
|
+
/** Bounds on both the incoming mdast tree and the converted tag tree. */
|
|
22
|
+
export declare const TAG_LIMITS: Readonly<{
|
|
23
|
+
treeDepth: 128;
|
|
24
|
+
treeNodes: 50000;
|
|
25
|
+
templateLength: 1000000;
|
|
26
|
+
}>;
|
|
27
|
+
export interface TagSyntaxOptions {
|
|
28
|
+
/** Capture raw opening-fence headers for host-owned header admission, including templates. */
|
|
29
|
+
fencedCodeHeaders?: WeakMap<Node, string>;
|
|
30
|
+
/** Opt in to variables, conditions, text templates, and bounded code templates. */
|
|
31
|
+
expressions?: boolean;
|
|
32
|
+
/** Named attributes that can encode LF, CR and TAB with backslash escapes. */
|
|
33
|
+
escapedWhitespace?: Readonly<Record<string, readonly string[]>>;
|
|
34
|
+
}
|
|
35
|
+
export type TagNode = TagTextNode | TagLeafNode | TagContainerNode | TagVariableNode | TagConditionalNode | TagCodeTemplateNode;
|
|
36
|
+
interface NamedTag extends Parent {
|
|
37
|
+
name: string;
|
|
38
|
+
attributes?: Record<string, TagAttributeValue | null | undefined>;
|
|
39
|
+
}
|
|
40
|
+
export interface TagTextNode extends NamedTag {
|
|
41
|
+
type: "tagText";
|
|
42
|
+
children: PhrasingContent[];
|
|
43
|
+
}
|
|
44
|
+
export interface TagLeafNode extends NamedTag {
|
|
45
|
+
type: "tagLeaf";
|
|
46
|
+
children: [];
|
|
47
|
+
}
|
|
48
|
+
export interface TagContainerNode extends NamedTag {
|
|
49
|
+
type: "tagContainer";
|
|
50
|
+
children: Array<BlockContent | DefinitionContent>;
|
|
51
|
+
}
|
|
52
|
+
export interface TagVariableNode extends Node {
|
|
53
|
+
type: "tagVariable";
|
|
54
|
+
path: string;
|
|
55
|
+
}
|
|
56
|
+
export interface TagCodeTemplateNode extends Node {
|
|
57
|
+
type: "tagCodeTemplate";
|
|
58
|
+
lang?: string | null;
|
|
59
|
+
meta?: string | null;
|
|
60
|
+
template: TagTextTemplate;
|
|
61
|
+
}
|
|
62
|
+
export interface TagBranchNode extends Parent {
|
|
63
|
+
type: "tagBranch";
|
|
64
|
+
condition: string | null;
|
|
65
|
+
children: Array<BlockContent | DefinitionContent>;
|
|
66
|
+
}
|
|
67
|
+
export interface TagConditionalNode extends Parent {
|
|
68
|
+
type: "tagConditional";
|
|
69
|
+
children: TagBranchNode[];
|
|
70
|
+
}
|
|
71
|
+
declare module "mdast" {
|
|
72
|
+
interface PhrasingContentMap {
|
|
73
|
+
tagText: TagTextNode;
|
|
74
|
+
tagVariable: TagVariableNode;
|
|
75
|
+
}
|
|
76
|
+
interface BlockContentMap {
|
|
77
|
+
tagLeaf: TagLeafNode;
|
|
78
|
+
tagContainer: TagContainerNode;
|
|
79
|
+
tagConditional: TagConditionalNode;
|
|
80
|
+
tagCodeTemplate: TagCodeTemplateNode;
|
|
81
|
+
}
|
|
82
|
+
interface RootContentMap {
|
|
83
|
+
tagBranch: TagBranchNode;
|
|
84
|
+
tagText: TagTextNode;
|
|
85
|
+
tagLeaf: TagLeafNode;
|
|
86
|
+
tagContainer: TagContainerNode;
|
|
87
|
+
tagVariable: TagVariableNode;
|
|
88
|
+
tagConditional: TagConditionalNode;
|
|
89
|
+
tagCodeTemplate: TagCodeTemplateNode;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export interface TagDiagnostic {
|
|
93
|
+
id?: string;
|
|
94
|
+
option?: string;
|
|
95
|
+
message: string;
|
|
96
|
+
line?: number;
|
|
97
|
+
column?: number;
|
|
98
|
+
}
|
|
99
|
+
export declare class TagSyntaxError extends Error {
|
|
100
|
+
readonly diagnostics: TagDiagnostic[];
|
|
101
|
+
constructor(diagnostics: TagDiagnostic[]);
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/syntax.d.ts
|
|
105
|
+
declare module "micromark-util-types" {
|
|
106
|
+
interface TokenTypeMap {
|
|
107
|
+
topikTextTag: "topikTextTag";
|
|
108
|
+
topikFlowTag: "topikFlowTag";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Recognize markers without consuming the Markdown between paired tags. */
|
|
112
|
+
export declare function tagSyntax(declarations: TagDeclarations, options?: TagSyntaxOptions): Extension$1;
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/from-markdown.d.ts
|
|
115
|
+
/** Build tag nodes after Markdown has established block and phrasing boundaries. */
|
|
116
|
+
export declare function tagFromMarkdown(declarations: TagDeclarations, options?: TagSyntaxOptions): Extension;
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/to-markdown.d.ts
|
|
119
|
+
declare module "mdast-util-to-markdown" {
|
|
120
|
+
interface ConstructNameMap {
|
|
121
|
+
tagText: "tagText";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Serialize tag nodes using the host writer for their Markdown children. */
|
|
125
|
+
export declare function tagToMarkdown(declarations: TagDeclarations, options?: TagSyntaxOptions): Options;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/templates.d.ts
|
|
128
|
+
/** Normalize complete literal runs before escaping, including caller-constructed segments. */
|
|
129
|
+
export declare function writeTextTemplate(template: unknown, code?: boolean): string;
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/index.d.ts
|
|
132
|
+
declare module "unified" {
|
|
133
|
+
interface Data {
|
|
134
|
+
micromarkExtensions?: Extension$1[];
|
|
135
|
+
fromMarkdownExtensions?: Array<Extension[] | Extension>;
|
|
136
|
+
toMarkdownExtensions?: Options[];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Add declarative tag parsing and serialization to a remark processor. */
|
|
140
|
+
export declare const remarkTags: Plugin<[TagDeclarations, TagSyntaxOptions?], Root>;
|
|
141
|
+
//#endregion
|
|
142
|
+
export { remarkTags as default };
|