@johnhenry/sitefile 0.0.0-stage → 0.0.0
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/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +475 -2
- package/lib/src/archive-writers.d.ts +31 -0
- package/lib/src/archive-writers.js +197 -0
- package/lib/src/attest.d.ts +126 -0
- package/lib/src/attest.js +220 -0
- package/lib/src/bake.d.ts +74 -0
- package/lib/src/bake.js +372 -0
- package/lib/src/base-image.d.ts +76 -0
- package/lib/src/base-image.js +138 -0
- package/lib/src/build-args.d.ts +26 -0
- package/lib/src/build-args.js +164 -0
- package/lib/src/cache-limit.d.ts +54 -0
- package/lib/src/cache-limit.js +239 -0
- package/lib/src/casefold-data.d.ts +2 -0
- package/lib/src/casefold-data.js +217 -0
- package/lib/src/cbor.d.ts +39 -0
- package/lib/src/cbor.js +174 -0
- package/lib/src/checklinks.d.ts +42 -0
- package/lib/src/checklinks.js +118 -0
- package/lib/src/cli/bake.d.ts +2 -0
- package/lib/src/cli/bake.js +179 -0
- package/lib/src/cli/build-image.d.ts +21 -0
- package/lib/src/cli/build-image.js +42 -0
- package/lib/src/cli/cache.d.ts +8 -0
- package/lib/src/cli/cache.js +109 -0
- package/lib/src/cli/common.d.ts +29 -0
- package/lib/src/cli/common.js +100 -0
- package/lib/src/cli/git-context.d.ts +18 -0
- package/lib/src/cli/git-context.js +48 -0
- package/lib/src/cli/images.d.ts +6 -0
- package/lib/src/cli/images.js +285 -0
- package/lib/src/cli/index.d.ts +10 -0
- package/lib/src/cli/index.js +95 -0
- package/lib/src/cli/outdated.d.ts +5 -0
- package/lib/src/cli/outdated.js +94 -0
- package/lib/src/cli/registry.d.ts +2 -0
- package/lib/src/cli/registry.js +122 -0
- package/lib/src/cli/serve-worker.d.ts +2 -0
- package/lib/src/cli/serve-worker.js +42 -0
- package/lib/src/cli/servers.d.ts +15 -0
- package/lib/src/cli/servers.js +451 -0
- package/lib/src/cli/vendor.d.ts +2 -0
- package/lib/src/cli/vendor.js +92 -0
- package/lib/src/cli/watch.d.ts +26 -0
- package/lib/src/cli/watch.js +81 -0
- package/lib/src/cli.d.ts +12 -0
- package/lib/src/cli.js +486 -0
- package/lib/src/csv.d.ts +27 -0
- package/lib/src/csv.js +117 -0
- package/lib/src/deflate.d.ts +76 -0
- package/lib/src/deflate.js +604 -0
- package/lib/src/digest.d.ts +3 -0
- package/lib/src/digest.js +7 -0
- package/lib/src/errors.d.ts +34 -0
- package/lib/src/errors.js +20 -0
- package/lib/src/executor.d.ts +386 -0
- package/lib/src/executor.js +2193 -0
- package/lib/src/explain.d.ts +64 -0
- package/lib/src/explain.js +445 -0
- package/lib/src/export-formats.d.ts +16 -0
- package/lib/src/export-formats.js +52 -0
- package/lib/src/exporter.d.ts +103 -0
- package/lib/src/exporter.js +676 -0
- package/lib/src/fingerprint.d.ts +40 -0
- package/lib/src/fingerprint.js +168 -0
- package/lib/src/frontmatter.d.ts +22 -0
- package/lib/src/frontmatter.js +45 -0
- package/lib/src/index.d.ts +12 -0
- package/lib/src/index.js +12 -0
- package/lib/src/language/ast.d.ts +353 -0
- package/lib/src/language/ast.js +57 -0
- package/lib/src/language/parser.d.ts +3 -0
- package/lib/src/language/parser.js +612 -0
- package/lib/src/language/scanner.d.ts +110 -0
- package/lib/src/language/scanner.js +449 -0
- package/lib/src/language/validate.d.ts +13 -0
- package/lib/src/language/validate.js +367 -0
- package/lib/src/links.d.ts +79 -0
- package/lib/src/links.js +583 -0
- package/lib/src/lint.d.ts +10 -0
- package/lib/src/lint.js +223 -0
- package/lib/src/markdown.d.ts +29 -0
- package/lib/src/markdown.js +612 -0
- package/lib/src/media.d.ts +82 -0
- package/lib/src/media.js +650 -0
- package/lib/src/memo.d.ts +57 -0
- package/lib/src/memo.js +93 -0
- package/lib/src/oci.d.ts +178 -0
- package/lib/src/oci.js +380 -0
- package/lib/src/outdated.d.ts +46 -0
- package/lib/src/outdated.js +195 -0
- package/lib/src/pages.d.ts +10 -0
- package/lib/src/pages.js +54 -0
- package/lib/src/paths.d.ts +39 -0
- package/lib/src/paths.js +101 -0
- package/lib/src/planner.d.ts +297 -0
- package/lib/src/planner.js +410 -0
- package/lib/src/plugins/cache.d.ts +70 -0
- package/lib/src/plugins/cache.js +226 -0
- package/lib/src/plugins/exec.d.ts +51 -0
- package/lib/src/plugins/exec.js +144 -0
- package/lib/src/plugins/host.d.ts +75 -0
- package/lib/src/plugins/host.js +277 -0
- package/lib/src/plugins/wasm.d.ts +23 -0
- package/lib/src/plugins/wasm.js +249 -0
- package/lib/src/profile.d.ts +42 -0
- package/lib/src/profile.js +68 -0
- package/lib/src/ref.d.ts +40 -0
- package/lib/src/ref.js +79 -0
- package/lib/src/registry-auth.d.ts +32 -0
- package/lib/src/registry-auth.js +151 -0
- package/lib/src/registry-cache.d.ts +71 -0
- package/lib/src/registry-cache.js +333 -0
- package/lib/src/registry-ops.d.ts +51 -0
- package/lib/src/registry-ops.js +248 -0
- package/lib/src/registry.d.ts +109 -0
- package/lib/src/registry.js +505 -0
- package/lib/src/render.d.ts +134 -0
- package/lib/src/render.js +576 -0
- package/lib/src/resources/archive.d.ts +11 -0
- package/lib/src/resources/archive.js +306 -0
- package/lib/src/resources/bundle-data.d.ts +16 -0
- package/lib/src/resources/bundle-data.js +50 -0
- package/lib/src/resources/convert.d.ts +12 -0
- package/lib/src/resources/convert.js +63 -0
- package/lib/src/resources/documents.d.ts +35 -0
- package/lib/src/resources/documents.js +122 -0
- package/lib/src/resources/git.d.ts +103 -0
- package/lib/src/resources/git.js +720 -0
- package/lib/src/resources/https.d.ts +40 -0
- package/lib/src/resources/https.js +256 -0
- package/lib/src/resources/ignore.d.ts +33 -0
- package/lib/src/resources/ignore.js +119 -0
- package/lib/src/resources/local.d.ts +64 -0
- package/lib/src/resources/local.js +248 -0
- package/lib/src/resources/lockfile.d.ts +52 -0
- package/lib/src/resources/lockfile.js +172 -0
- package/lib/src/resources/netpolicy.d.ts +6 -0
- package/lib/src/resources/netpolicy.js +82 -0
- package/lib/src/resources/types.d.ts +63 -0
- package/lib/src/resources/types.js +13 -0
- package/lib/src/resources/vendor.d.ts +94 -0
- package/lib/src/resources/vendor.js +296 -0
- package/lib/src/resources/wbn-reader.d.ts +44 -0
- package/lib/src/resources/wbn-reader.js +302 -0
- package/lib/src/schema.d.ts +25 -0
- package/lib/src/schema.js +169 -0
- package/lib/src/secrets.d.ts +21 -0
- package/lib/src/secrets.js +103 -0
- package/lib/src/serve-rules.d.ts +29 -0
- package/lib/src/serve-rules.js +122 -0
- package/lib/src/server.d.ts +45 -0
- package/lib/src/server.js +246 -0
- package/lib/src/servers.d.ts +71 -0
- package/lib/src/servers.js +139 -0
- package/lib/src/site.d.ts +60 -0
- package/lib/src/site.js +169 -0
- package/lib/src/store.d.ts +151 -0
- package/lib/src/store.js +596 -0
- package/lib/src/tree.d.ts +50 -0
- package/lib/src/tree.js +134 -0
- package/lib/src/values.d.ts +7 -0
- package/lib/src/values.js +37 -0
- package/lib/src/vendor.d.ts +19 -0
- package/lib/src/vendor.js +145 -0
- package/lib/src/watch.d.ts +44 -0
- package/lib/src/watch.js +137 -0
- package/lib/src/wbn.d.ts +5 -0
- package/lib/src/wbn.js +147 -0
- package/lib/src/yaml.d.ts +34 -0
- package/lib/src/yaml.js +562 -0
- package/package.json +58 -4
package/lib/src/lint.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { isFragment, isInline } from './language/ast.js';
|
|
2
|
+
import { logicalLines } from './language/scanner.js';
|
|
3
|
+
import { operandsOf, parseFragmentBody } from './language/validate.js';
|
|
4
|
+
import { templateNames } from './executor.js';
|
|
5
|
+
import { importedStageRefs } from './planner.js';
|
|
6
|
+
import { parseRenderTemplate } from './render.js';
|
|
7
|
+
import { parseRef } from './ref.js';
|
|
8
|
+
/** Every rule, in the order warnings of one line are reported. */
|
|
9
|
+
export const LINT_RULES = ['unused-name', 'unpinned-import', 'unpinned-image', 'render-escape', 'sitemap-order', 'shadowed-write'];
|
|
10
|
+
/** Import kinds a RENDER template can read (so a template file may use them). */
|
|
11
|
+
const VALUE_KINDS = new Set(['text', 'json', 'yaml', 'csv', 'markdown', 'wbn', 'html']);
|
|
12
|
+
/** `# sitefile-lint: …` (name ASCII case-insensitive): group 1 is the rest of the comment. */
|
|
13
|
+
const LINT_COMMENT = /^#[ \t]*[Ss][Ii][Tt][Ee][Ff][Ii][Ll][Ee]-[Ll][Ii][Nn][Tt][ \t]*:([\s\S]*)$/;
|
|
14
|
+
const LINT_BODY = /^(disable|disable-next-line)[ \t]*=[ \t]*([\s\S]+)$/;
|
|
15
|
+
/** Text of an operand as written (`${…}` kept), for messages and extension checks. */
|
|
16
|
+
function written(op) {
|
|
17
|
+
return op.parts.map((p) => (p.kind === 'text' ? p.value : `\${${[p.name, ...p.path].join('.')}}`)).join('');
|
|
18
|
+
}
|
|
19
|
+
/** Every operand of an instruction, EXPORT's BASE and AT included. */
|
|
20
|
+
function operands(ins) {
|
|
21
|
+
const ops = operandsOf(ins);
|
|
22
|
+
if (ins.kind === 'EXPORT')
|
|
23
|
+
ops.push(...(ins.base ? [ins.base] : []), ...(ins.at ? [ins.at] : []));
|
|
24
|
+
return ops;
|
|
25
|
+
}
|
|
26
|
+
/** Parse `# sitefile-lint:` comments: rules off for the file, rules off per line, and problems. */
|
|
27
|
+
function directives(comments, lines, found) {
|
|
28
|
+
const file = new Set();
|
|
29
|
+
const perLine = new Map();
|
|
30
|
+
for (const c of comments) {
|
|
31
|
+
const m = LINT_COMMENT.exec(c.text);
|
|
32
|
+
if (!m)
|
|
33
|
+
continue;
|
|
34
|
+
const body = LINT_BODY.exec(m[1].trim());
|
|
35
|
+
if (!body) {
|
|
36
|
+
found.push({ line: c.line, rule: 'lint-comment', message: 'unrecognized # sitefile-lint comment (expected disable=<rule>[,<rule>…] or disable-next-line=<rule>[,<rule>…])' });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const ids = body[2].split(',').map((s) => s.trim());
|
|
40
|
+
const known = ids.filter((id) => {
|
|
41
|
+
if (id === 'all' || LINT_RULES.includes(id))
|
|
42
|
+
return true;
|
|
43
|
+
found.push({ line: c.line, rule: 'lint-comment', message: `unknown lint rule '${id}' (rules: ${LINT_RULES.join(', ')}, or all)` });
|
|
44
|
+
return false;
|
|
45
|
+
});
|
|
46
|
+
if (body[1] === 'disable') {
|
|
47
|
+
known.forEach((id) => file.add(id));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// The next instruction: the first logical line that starts after the comment.
|
|
51
|
+
const next = lines.find((l) => (l.positions[0]?.line ?? 0) > c.line);
|
|
52
|
+
if (!next)
|
|
53
|
+
continue;
|
|
54
|
+
const at = next.positions[0].line;
|
|
55
|
+
const set = perLine.get(at) ?? new Set();
|
|
56
|
+
known.forEach((id) => set.add(id));
|
|
57
|
+
perLine.set(at, set);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { file, lines: perLine };
|
|
61
|
+
}
|
|
62
|
+
/** Whether a RENDER line sets `--escape=` explicitly (before its template). */
|
|
63
|
+
function explicitEscape(line) {
|
|
64
|
+
if (!line)
|
|
65
|
+
return false;
|
|
66
|
+
const words = line.text.split(/\s+/).filter((w) => w !== '');
|
|
67
|
+
for (const w of words.slice(1)) {
|
|
68
|
+
if (!w.startsWith('--'))
|
|
69
|
+
break;
|
|
70
|
+
if (w.startsWith('--escape='))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/** The file path a RENDER or COPY writes, when it is known before the build (otherwise undefined). */
|
|
76
|
+
function writtenPath(op) {
|
|
77
|
+
if (op.kind === 'RENDER')
|
|
78
|
+
return op.each ? written(op.each.dest) : op.dest;
|
|
79
|
+
if (op.kind === 'COPY' && !op.destIsDir)
|
|
80
|
+
return op.dest;
|
|
81
|
+
if (op.kind === 'TRANSFORM')
|
|
82
|
+
return written(op.dest);
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Lint a validated program and its plan. `source` is the Sitefile text (for comments),
|
|
87
|
+
* `disabled` the rule ids turned off by the caller (`--lint-disable`).
|
|
88
|
+
*/
|
|
89
|
+
export function lint(program, plan, source, disabled = []) {
|
|
90
|
+
const file = program.file;
|
|
91
|
+
const comments = [];
|
|
92
|
+
const lines = logicalLines(source, file, comments);
|
|
93
|
+
const byStart = new Map(lines.map((l) => [`${l.positions[0]?.line}:${l.positions[0]?.column}`, l]));
|
|
94
|
+
const found = [];
|
|
95
|
+
const { file: offInFile, lines: offPerLine } = directives(comments, lines, found);
|
|
96
|
+
const off = new Set([...offInFile, ...disabled]);
|
|
97
|
+
const report = (line, rule, message) => {
|
|
98
|
+
found.push({ line, rule, message });
|
|
99
|
+
};
|
|
100
|
+
// ---- unused-name: a declaration nothing refers to.
|
|
101
|
+
const used = new Set();
|
|
102
|
+
// A template file (or one from an import or stage) can read any ARG, LET or value import.
|
|
103
|
+
let opaqueTemplates = false;
|
|
104
|
+
for (const ins of program.instructions) {
|
|
105
|
+
for (const op of operands(ins))
|
|
106
|
+
for (const p of op.parts)
|
|
107
|
+
if (p.kind === 'var')
|
|
108
|
+
used.add(p.name);
|
|
109
|
+
if (ins.kind === 'RENDER') {
|
|
110
|
+
if (isInline(ins.template)) {
|
|
111
|
+
try {
|
|
112
|
+
const body = ins.template;
|
|
113
|
+
templateNames(parseRenderTemplate(body.text, file, body.bodyLine)).forEach((n) => used.add(n));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
opaqueTemplates = true; // a template error is reported by the build; assume it may use anything
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
opaqueTemplates = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// An html LET's template names what it uses (validate already parsed it).
|
|
124
|
+
if (isFragment(ins))
|
|
125
|
+
templateNames(parseFragmentBody(ins.body, file)).forEach((n) => used.add(n));
|
|
126
|
+
if ((ins.kind === 'RENDER' || ins.kind === 'TRANSFORM') && ins.each)
|
|
127
|
+
used.add(ins.each.list);
|
|
128
|
+
if (ins.kind === 'FEED')
|
|
129
|
+
used.add(ins.list);
|
|
130
|
+
if (ins.kind === 'TRANSFORM')
|
|
131
|
+
used.add(ins.plugin);
|
|
132
|
+
}
|
|
133
|
+
for (const stage of plan.stages) {
|
|
134
|
+
for (const op of stage.operations) {
|
|
135
|
+
const src = op.kind === 'COPY' ? op.source : op.kind === 'RENDER' ? op.template : op.kind === 'TRANSFORM' ? op.source : undefined;
|
|
136
|
+
if (src?.kind === 'import')
|
|
137
|
+
used.add(src.name);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// An IMPORT … AS sitefile is used by naming one of its stages (`FROM theme+assets`, `--from=theme+assets`).
|
|
141
|
+
for (const ref of importedStageRefs(plan.stages))
|
|
142
|
+
used.add(ref.import);
|
|
143
|
+
for (const ins of program.instructions) {
|
|
144
|
+
let what;
|
|
145
|
+
let name = '';
|
|
146
|
+
if (ins.kind === 'ARG' || ins.kind === 'LET') {
|
|
147
|
+
if (!opaqueTemplates)
|
|
148
|
+
[what, name] = [ins.kind, ins.name];
|
|
149
|
+
}
|
|
150
|
+
else if (ins.kind === 'IMPORT') {
|
|
151
|
+
if (!opaqueTemplates || !VALUE_KINDS.has(ins.as ?? ''))
|
|
152
|
+
[what, name] = ['IMPORT', ins.name];
|
|
153
|
+
}
|
|
154
|
+
else if (ins.kind === 'PLUGIN' || ins.kind === 'TOOL') {
|
|
155
|
+
[what, name] = [ins.kind, ins.name];
|
|
156
|
+
}
|
|
157
|
+
if (what && !used.has(name))
|
|
158
|
+
report(ins.span.line, 'unused-name', `${what} ${name} is never used`);
|
|
159
|
+
}
|
|
160
|
+
// ---- unpinned-import: a remote IMPORT without INTEGRITY.
|
|
161
|
+
for (const spec of plan.imports) {
|
|
162
|
+
if (spec.location.kind === 'remote' && !spec.integrity) {
|
|
163
|
+
report(spec.span.line, 'unpinned-import', `IMPORT ${spec.name} has no INTEGRITY, so ${spec.location.url} is trusted on first use (pin it with INTEGRITY sha256:…)`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ---- unpinned-image: a registry image (FROM image, an oci EXPORT BASE) without a digest.
|
|
167
|
+
const pinned = (ref) => {
|
|
168
|
+
try {
|
|
169
|
+
return parseRef(ref).digest !== undefined;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return undefined; // not a reference: the build reports it
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const unpinnedImage = (what, ref) => `${what} ${ref} has no @sha256: digest, so the tag is trusted on first use (pin it as ${ref}@sha256:…)`;
|
|
176
|
+
for (const stage of plan.stages) {
|
|
177
|
+
if (stage.base.kind === 'image' && !stage.base.local && pinned(stage.base.ref) === false) {
|
|
178
|
+
report(stage.span.line, 'unpinned-image', unpinnedImage(`${stage.base.lead} image`, stage.base.ref));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const exp = program.instructions.find((i) => i.kind === 'EXPORT');
|
|
182
|
+
if (exp && plan.exportFormat === 'oci' && plan.exportBase !== undefined && pinned(plan.exportBase) === false) {
|
|
183
|
+
report(exp.span.line, 'unpinned-image', unpinnedImage('EXPORT … AS oci BASE', plan.exportBase));
|
|
184
|
+
}
|
|
185
|
+
for (const stage of plan.stages) {
|
|
186
|
+
const ops = stage.operations;
|
|
187
|
+
// ---- render-escape: an HTML page rendered without escaping.
|
|
188
|
+
for (const op of ops) {
|
|
189
|
+
if (op.kind !== 'RENDER' || op.escape !== 'none')
|
|
190
|
+
continue;
|
|
191
|
+
const dest = writtenPath(op);
|
|
192
|
+
if (!/\.html?$/i.test(dest) || explicitEscape(byStart.get(`${op.span.line}:${op.span.column}`)))
|
|
193
|
+
continue;
|
|
194
|
+
report(op.span.line, 'render-escape', `RENDER writes ${dest} without --escape=html, so substituted values are not HTML-escaped (add --escape=html, or --escape=none if that is intended)`);
|
|
195
|
+
}
|
|
196
|
+
// ---- sitemap-order: a page written after the SITEMAP that should list it.
|
|
197
|
+
ops.forEach((op, i) => {
|
|
198
|
+
if (op.kind !== 'SITEMAP')
|
|
199
|
+
return;
|
|
200
|
+
const later = ops.slice(i + 1).find((o) => writtenPath(o)?.endsWith('.html'));
|
|
201
|
+
if (later) {
|
|
202
|
+
report(op.span.line, 'sitemap-order', `SITEMAP lists the .html files written before it, but line ${later.span.line} writes ${writtenPath(later)} after it (move SITEMAP after the pages)`);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
// ---- shadowed-write: a file replaced by a later --overwrite before anything reads it.
|
|
206
|
+
const last = new Map();
|
|
207
|
+
for (const op of ops) {
|
|
208
|
+
const path = op.kind === 'RENDER' && !op.each ? op.dest : op.kind === 'COPY' && op.source.kind === 'inline' ? op.dest : undefined;
|
|
209
|
+
if (path === undefined)
|
|
210
|
+
continue;
|
|
211
|
+
const before = last.get(path);
|
|
212
|
+
if (before !== undefined && 'overwrite' in op && op.overwrite) {
|
|
213
|
+
report(before, 'shadowed-write', `${path} written here is replaced by line ${op.span.line} (--overwrite), so this write has no effect`);
|
|
214
|
+
}
|
|
215
|
+
last.set(path, op.span.line);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const kept = found.filter((f) => f.rule === 'lint-comment' || !(off.has('all') || off.has(f.rule) || offPerLine.get(f.line)?.has('all') || offPerLine.get(f.line)?.has(f.rule)));
|
|
219
|
+
// Stable: by line, then in the order the rules ran.
|
|
220
|
+
const sorted = kept.map((f, i) => ({ f, i })).sort((a, b) => a.f.line - b.f.line || a.i - b.i);
|
|
221
|
+
return sorted.map(({ f }) => `${file}:${f.line}: [${f.rule}] ${f.message}`);
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=lint.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, dependency-free, deterministic Markdown renderer (CommonMark-ish).
|
|
3
|
+
*
|
|
4
|
+
* Blocks: ATX headings (#..######, optional closing #s), setext headings,
|
|
5
|
+
* paragraphs (hard break = two trailing spaces or a trailing backslash),
|
|
6
|
+
* fenced code (``` and ~~~, info word becomes class="language-x" when it is
|
|
7
|
+
* [A-Za-z0-9_+-]+), indented code, blockquotes (recursive, lazy continuation),
|
|
8
|
+
* bullet lists (- * +) and ordered lists (1. / 1), `start` kept when not 1),
|
|
9
|
+
* nesting by indentation, tight vs loose lists, thematic breaks (--- *** ___).
|
|
10
|
+
*
|
|
11
|
+
* Inlines: backslash escapes, code spans, **strong** / __strong__, *em* /
|
|
12
|
+
* _em_ (intraword `_` is literal), ~~del~~, [text](url "title"),
|
|
13
|
+
* , <autolinks>. Emphasis that does not close stays literal.
|
|
14
|
+
*
|
|
15
|
+
* Not supported: reference links, HTML entities (`&` is always escaped, so
|
|
16
|
+
* `&` shows literally), tables, footnotes, task lists.
|
|
17
|
+
*
|
|
18
|
+
* Safety: raw HTML is never passed through; every `<` in the source is
|
|
19
|
+
* escaped. Link and image URLs must be http, https, mailto, relative,
|
|
20
|
+
* `#fragment` or `/absolute`; anything else (javascript:, data:, ...) is
|
|
21
|
+
* rendered as plain text without an <a> / <img>.
|
|
22
|
+
*/
|
|
23
|
+
export interface MarkdownResult {
|
|
24
|
+
html: string;
|
|
25
|
+
/** Plain text of the first paragraph, at most 200 characters. */
|
|
26
|
+
excerpt: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function escapeHtml(s: string): string;
|
|
29
|
+
export declare function renderMarkdown(src: string): MarkdownResult;
|