@chromamark/renderer 0.2.0 → 0.2.2
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/dist/chromamark.esm.js +88 -19
- package/dist/chromamark.min.js +8 -8
- package/package.json +1 -1
- package/src/browser-core.js +17 -6
- package/src/containers.js +88 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chromamark/renderer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "ChromaMark renderer — a markdown-it plugin adding colored blocks, colored pills, collapsible sections, fields, meters and inline diff on top of CommonMark + GFM.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "cjfravel-dev",
|
package/src/browser-core.js
CHANGED
|
@@ -36,18 +36,29 @@ export function injectTheme(doc) {
|
|
|
36
36
|
(d.head || d.documentElement).appendChild(style);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/** Strip shared leading indentation so source can be indented inside HTML.
|
|
39
|
+
/** Strip shared leading indentation so source can be indented inside HTML.
|
|
40
|
+
* Only the common leading-whitespace prefix is removed; tabs that appear in
|
|
41
|
+
* content (e.g. inside a code span) are preserved. */
|
|
40
42
|
function dedent(text) {
|
|
41
|
-
const lines = text.replace(/\
|
|
43
|
+
const lines = text.replace(/\r/g, '').split('\n');
|
|
42
44
|
while (lines.length && lines[0].trim() === '') lines.shift();
|
|
43
45
|
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
|
|
44
|
-
let
|
|
46
|
+
let prefix = null;
|
|
45
47
|
for (const line of lines) {
|
|
46
48
|
if (!line.trim()) continue;
|
|
47
|
-
|
|
49
|
+
const lead = line.match(/^[ \t]*/)[0];
|
|
50
|
+
if (prefix === null) {
|
|
51
|
+
prefix = lead;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
let i = 0;
|
|
55
|
+
const lim = Math.min(prefix.length, lead.length);
|
|
56
|
+
while (i < lim && prefix[i] === lead[i]) i++;
|
|
57
|
+
prefix = prefix.slice(0, i);
|
|
58
|
+
if (prefix === '') break;
|
|
48
59
|
}
|
|
49
|
-
|
|
50
|
-
return lines.map((line) => line.slice(
|
|
60
|
+
const cut = prefix ? prefix.length : 0;
|
|
61
|
+
return lines.map((line) => line.slice(cut)).join('\n');
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
function resolve(target) {
|
package/src/containers.js
CHANGED
|
@@ -95,13 +95,56 @@ function makeRule(enabled) {
|
|
|
95
95
|
|
|
96
96
|
let nextLine = startLine;
|
|
97
97
|
let autoClosed = false;
|
|
98
|
+
let fenceCh = 0;
|
|
99
|
+
let fenceLen = 0;
|
|
98
100
|
for (;;) {
|
|
99
101
|
nextLine++;
|
|
100
102
|
if (nextLine >= endLine) break;
|
|
101
103
|
const lstart = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
102
104
|
const lmax = state.eMarks[nextLine];
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
const indent = state.sCount[nextLine] - state.blkIndent;
|
|
106
|
+
|
|
107
|
+
// Skip over fenced code blocks so a ::: (or fence) line inside one counts
|
|
108
|
+
// as content, not as the container's closing fence.
|
|
109
|
+
if (fenceCh) {
|
|
110
|
+
if (indent < 4 && state.src.charCodeAt(lstart) === fenceCh) {
|
|
111
|
+
let q = lstart;
|
|
112
|
+
while (q < lmax && state.src.charCodeAt(q) === fenceCh) q++;
|
|
113
|
+
if (q - lstart >= fenceLen) {
|
|
114
|
+
let r = q;
|
|
115
|
+
while (r < lmax && (state.src.charCodeAt(r) === 0x20 || state.src.charCodeAt(r) === 0x09)) r++;
|
|
116
|
+
if (r >= lmax) {
|
|
117
|
+
fenceCh = 0;
|
|
118
|
+
fenceLen = 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const openCh = state.src.charCodeAt(lstart);
|
|
125
|
+
if (indent < 4 && (openCh === 0x60 || openCh === 0x7e)) {
|
|
126
|
+
let q = lstart;
|
|
127
|
+
while (q < lmax && state.src.charCodeAt(q) === openCh) q++;
|
|
128
|
+
if (q - lstart >= 3) {
|
|
129
|
+
let ok = true;
|
|
130
|
+
if (openCh === 0x60) {
|
|
131
|
+
for (let r = q; r < lmax; r++) {
|
|
132
|
+
if (state.src.charCodeAt(r) === 0x60) {
|
|
133
|
+
ok = false;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (ok) {
|
|
139
|
+
fenceCh = openCh;
|
|
140
|
+
fenceLen = q - lstart;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (openCh !== MARKER) continue;
|
|
147
|
+
if (indent >= 4) continue;
|
|
105
148
|
const closeLen = fenceLength(state.src, lstart, lmax);
|
|
106
149
|
if (closeLen < openLen) continue;
|
|
107
150
|
let p = lstart + closeLen;
|
|
@@ -154,6 +197,19 @@ export default function containerPlugin(md, enabled) {
|
|
|
154
197
|
});
|
|
155
198
|
const esc = md.utils.escapeHtml;
|
|
156
199
|
|
|
200
|
+
// Render a title/summary/field value as inline content (so pills, colored
|
|
201
|
+
// text, meters, and markdown work there) while forcing html:false, so raw
|
|
202
|
+
// HTML in that user-controlled text is escaped rather than injected.
|
|
203
|
+
const renderInlineSafe = (text) => {
|
|
204
|
+
const prevHtml = md.options.html;
|
|
205
|
+
md.options.html = false;
|
|
206
|
+
try {
|
|
207
|
+
return md.renderInline(text);
|
|
208
|
+
} finally {
|
|
209
|
+
md.options.html = prevHtml;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
157
213
|
function decorate(meta) {
|
|
158
214
|
const custom = meta.color ? ' cm-custom' : '';
|
|
159
215
|
const style = meta.color ? ` style="--fg:${esc(meta.color)}"` : '';
|
|
@@ -168,11 +224,11 @@ export default function containerPlugin(md, enabled) {
|
|
|
168
224
|
const open = meta.open ? ' open' : '';
|
|
169
225
|
return (
|
|
170
226
|
`<details class="cm-details${custom}"${tone}${style}${open}>` +
|
|
171
|
-
`<summary>${
|
|
227
|
+
`<summary>${renderInlineSafe(meta.summary)}</summary><div class="cm-body">`
|
|
172
228
|
);
|
|
173
229
|
}
|
|
174
230
|
let html = `<div class="cm-block${custom}"${tone}${style}>`;
|
|
175
|
-
if (meta.title) html += `<div class="cm-title">${
|
|
231
|
+
if (meta.title) html += `<div class="cm-title">${renderInlineSafe(meta.title)}</div>`;
|
|
176
232
|
return html + '<div class="cm-body">';
|
|
177
233
|
};
|
|
178
234
|
|
|
@@ -180,18 +236,34 @@ export default function containerPlugin(md, enabled) {
|
|
|
180
236
|
tokens[idx].meta.structure === 'details' ? '</div></details>' : '</div></div>';
|
|
181
237
|
|
|
182
238
|
md.renderer.rules.cm_fields = (tokens, idx) => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
md.options.html = false;
|
|
187
|
-
try {
|
|
188
|
-
let html = '<dl class="cm-fields">';
|
|
189
|
-
for (const [key, value] of tokens[idx].meta.rows) {
|
|
190
|
-
html += `<dt>${esc(key)}</dt><dd>${md.renderInline(value)}</dd>`;
|
|
191
|
-
}
|
|
192
|
-
return html + '</dl>';
|
|
193
|
-
} finally {
|
|
194
|
-
md.options.html = prevHtml;
|
|
239
|
+
let html = '<dl class="cm-fields">';
|
|
240
|
+
for (const [key, value] of tokens[idx].meta.rows) {
|
|
241
|
+
html += `<dt>${esc(key)}</dt><dd>${renderInlineSafe(value)}</dd>`;
|
|
195
242
|
}
|
|
243
|
+
return html + '</dl>';
|
|
196
244
|
};
|
|
245
|
+
|
|
246
|
+
// Container bodies are always safe: escape any raw HTML inside a container so
|
|
247
|
+
// ChromaMark stays consistent with its force-escaped titles/fields even when
|
|
248
|
+
// attached to a host MarkdownIt({ html: true }). Raw HTML outside a container
|
|
249
|
+
// still honors the host setting. Under the default html:false there are no
|
|
250
|
+
// html_block/html_inline tokens, so this is a no-op.
|
|
251
|
+
md.core.ruler.push('cm_sanitize_bodies', (state) => {
|
|
252
|
+
let depth = 0;
|
|
253
|
+
for (const token of state.tokens) {
|
|
254
|
+
if (token.type === 'cm_container_open') {
|
|
255
|
+
depth++;
|
|
256
|
+
} else if (token.type === 'cm_container_close') {
|
|
257
|
+
depth--;
|
|
258
|
+
} else if (depth > 0) {
|
|
259
|
+
if (token.type === 'html_block') {
|
|
260
|
+
token.content = esc(token.content);
|
|
261
|
+
} else if (token.type === 'inline' && token.children) {
|
|
262
|
+
for (const child of token.children) {
|
|
263
|
+
if (child.type === 'html_inline') child.content = esc(child.content);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
197
269
|
}
|