@chromamark/renderer 0.1.0 → 0.2.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/README.md +28 -2
- package/dist/chromamark.esm.js +79 -25
- package/dist/chromamark.min.js +10 -9
- package/package.json +1 -1
- package/src/browser-core.js +47 -3
- package/src/critic.js +15 -1
- package/src/inline.js +54 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chromamark/renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
@@ -8,8 +8,10 @@ import { createRenderer, render as renderString } from './index.js';
|
|
|
8
8
|
|
|
9
9
|
const STYLE_ID = 'chromamark-theme';
|
|
10
10
|
const DONE_ATTR = 'data-chromamark-done';
|
|
11
|
+
const SRC_ATTR = 'data-chromamark-src';
|
|
12
|
+
const ERR_ATTR = 'data-chromamark-error';
|
|
11
13
|
const DEFAULT_SELECTOR =
|
|
12
|
-
'script[type="text/chromamark"], template.chromamark, [data-chromamark], .chromamark';
|
|
14
|
+
'script[type="text/chromamark"], template.chromamark, [data-chromamark], [data-chromamark-src], .chromamark';
|
|
13
15
|
|
|
14
16
|
/** The theme stylesheet (populated by the bundle via configureTheme). */
|
|
15
17
|
export let theme = '';
|
|
@@ -67,16 +69,20 @@ function sourceOf(el, tag) {
|
|
|
67
69
|
export function renderElement(target, options) {
|
|
68
70
|
const el = resolve(target);
|
|
69
71
|
if (!el || el.hasAttribute(DONE_ATTR)) return null;
|
|
72
|
+
if (el.hasAttribute && el.hasAttribute(SRC_ATTR)) return renderSrc(el, options);
|
|
70
73
|
|
|
74
|
+
const doc = el.ownerDocument || (typeof document !== 'undefined' ? document : null);
|
|
71
75
|
const tag = (el.tagName || '').toLowerCase();
|
|
72
76
|
const html = renderString(dedent(sourceOf(el, tag)), options);
|
|
73
77
|
el.setAttribute(DONE_ATTR, '');
|
|
74
78
|
|
|
75
79
|
if (tag === 'script' || tag === 'template') {
|
|
76
|
-
const out =
|
|
80
|
+
const out = doc.createElement('div');
|
|
77
81
|
out.className = 'chromamark-output';
|
|
78
82
|
out.innerHTML = html;
|
|
79
|
-
|
|
83
|
+
// A detached holder has no parent to receive a sibling; return the
|
|
84
|
+
// rendered node anyway so the caller can place it, rather than crashing.
|
|
85
|
+
if (el.parentNode) el.parentNode.insertBefore(out, el.nextSibling);
|
|
80
86
|
return out;
|
|
81
87
|
}
|
|
82
88
|
el.innerHTML = html;
|
|
@@ -84,6 +90,43 @@ export function renderElement(target, options) {
|
|
|
84
90
|
return el;
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Fetch the external ChromaMark file named by an element's data-chromamark-src
|
|
95
|
+
* and render it into that element, the way a browser loads an external script or
|
|
96
|
+
* stylesheet. Resolves to the element on success, or null when skipped/failed;
|
|
97
|
+
* it never rejects, so a missing file degrades gracefully (the element keeps its
|
|
98
|
+
* existing content and gains a data-chromamark-error marker). Requires a DOM
|
|
99
|
+
* with fetch — i.e. the page must be served over http(s), not opened as file://.
|
|
100
|
+
*/
|
|
101
|
+
export function renderSrc(target, options) {
|
|
102
|
+
const el = resolve(target);
|
|
103
|
+
if (!el || el.hasAttribute(DONE_ATTR)) return Promise.resolve(null);
|
|
104
|
+
const url = el.getAttribute(SRC_ATTR);
|
|
105
|
+
if (!url) return Promise.resolve(null);
|
|
106
|
+
el.setAttribute(DONE_ATTR, '');
|
|
107
|
+
|
|
108
|
+
const view = el.ownerDocument && el.ownerDocument.defaultView;
|
|
109
|
+
const doFetch = (view && view.fetch) || (typeof fetch !== 'undefined' ? fetch : null);
|
|
110
|
+
const fail = (message) => {
|
|
111
|
+
el.setAttribute(ERR_ATTR, message);
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
if (!doFetch) return Promise.resolve(fail('ChromaMark: fetch is unavailable'));
|
|
115
|
+
|
|
116
|
+
return Promise.resolve()
|
|
117
|
+
.then(() => doFetch(url))
|
|
118
|
+
.then((res) => {
|
|
119
|
+
if (!res || !res.ok) throw new Error(`HTTP ${res ? res.status : '?'}`);
|
|
120
|
+
return res.text();
|
|
121
|
+
})
|
|
122
|
+
.then((text) => {
|
|
123
|
+
el.innerHTML = renderString(text, options);
|
|
124
|
+
el.classList.add('chromamark-output');
|
|
125
|
+
return el;
|
|
126
|
+
})
|
|
127
|
+
.catch((err) => fail(`ChromaMark: failed to load ${url} (${(err && err.message) || err})`));
|
|
128
|
+
}
|
|
129
|
+
|
|
87
130
|
/** Render every element matching the selector (defaults to the standard targets). */
|
|
88
131
|
export function renderAll(selector, options) {
|
|
89
132
|
const nodes = document.querySelectorAll(selector || DEFAULT_SELECTOR);
|
|
@@ -100,6 +143,7 @@ export const ChromaMark = {
|
|
|
100
143
|
render,
|
|
101
144
|
renderElement,
|
|
102
145
|
renderAll,
|
|
146
|
+
renderSrc,
|
|
103
147
|
injectTheme,
|
|
104
148
|
autoRender,
|
|
105
149
|
createRenderer,
|
package/src/critic.js
CHANGED
|
@@ -14,6 +14,20 @@ const KINDS = {
|
|
|
14
14
|
'>>': { kind: 'comment', close: '<<}' },
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* First index of the close token at or after `from`, memoized per token on the
|
|
19
|
+
* parse state so many unterminated openers on one line stay O(n) overall
|
|
20
|
+
* instead of each rescanning the tail (O(n^2)).
|
|
21
|
+
*/
|
|
22
|
+
function findClose(state, needle, from) {
|
|
23
|
+
const slot = '_cmCritic' + needle;
|
|
24
|
+
const c = state[slot];
|
|
25
|
+
if (c !== undefined && c.from <= from && (c.at === -1 || from <= c.at)) return c.at;
|
|
26
|
+
const at = state.src.indexOf(needle, from);
|
|
27
|
+
state[slot] = { from, at };
|
|
28
|
+
return at;
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
function criticRule(state, silent) {
|
|
18
32
|
const start = state.pos;
|
|
19
33
|
const src = state.src;
|
|
@@ -23,7 +37,7 @@ function criticRule(state, silent) {
|
|
|
23
37
|
if (!spec) return false;
|
|
24
38
|
|
|
25
39
|
const contentStart = start + 3;
|
|
26
|
-
const closeIdx =
|
|
40
|
+
const closeIdx = findClose(state, spec.close, contentStart);
|
|
27
41
|
if (closeIdx === -1 || closeIdx + spec.close.length > state.posMax) return false;
|
|
28
42
|
|
|
29
43
|
const raw = src.slice(contentStart, closeIdx);
|
package/src/inline.js
CHANGED
|
@@ -17,10 +17,21 @@ function unescape(text) {
|
|
|
17
17
|
return text.replace(/\\([\s\S])/g, '$1');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const WS = /\s/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Split an inline construct's body (state.src between `from` and `end`) into its
|
|
24
|
+
* leading spec token and the start of the remaining content, without slicing the
|
|
25
|
+
* whole — possibly huge — span first, so an invalid spec is rejected cheaply.
|
|
26
|
+
* Equivalent to matching `^(\S+)(?:\s+…)?$` on the trimmed body.
|
|
27
|
+
*/
|
|
28
|
+
function splitInline(src, from, end) {
|
|
29
|
+
let s = from;
|
|
30
|
+
while (s < end && WS.test(src[s])) s++;
|
|
31
|
+
let e = s;
|
|
32
|
+
while (e < end && !WS.test(src[e])) e++;
|
|
33
|
+
if (e === s) return null; // empty / all-whitespace body
|
|
34
|
+
return { specToken: src.slice(s, e), restStart: e };
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
/** Compute a meter fill width (0–100, as a string) from a value like "87%" or "3/10". */
|
|
@@ -41,15 +52,43 @@ function meterWidth(value) {
|
|
|
41
52
|
return String(+pct.toFixed(2));
|
|
42
53
|
}
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
/**
|
|
56
|
+
* First index of `needle` (a single char) at or after `from`, memoized on the
|
|
57
|
+
* parse state so N openers on one long line cost O(line) total rather than
|
|
58
|
+
* O(N·line). The cache is valid because `state.src` is constant for a state and
|
|
59
|
+
* the first occurrence at-or-after `from` is monotonic.
|
|
60
|
+
*/
|
|
61
|
+
function nextIndex(state, slot, needle, from) {
|
|
62
|
+
const c = state[slot];
|
|
63
|
+
if (c !== undefined && c.from <= from && (c.at === -1 || from <= c.at)) return c.at;
|
|
64
|
+
const at = state.src.indexOf(needle, from);
|
|
65
|
+
state[slot] = { from, at };
|
|
66
|
+
return at;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** True if `src[pos]` is escaped by an odd run of backslashes back to `floor`. */
|
|
70
|
+
function isEscaped(src, pos, floor) {
|
|
71
|
+
let n = 0;
|
|
72
|
+
for (let i = pos - 1; i >= floor && src.charCodeAt(i) === 0x5c; i--) n++;
|
|
73
|
+
return (n & 1) === 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** First unescaped `needle` at or after `from` (escapes counted from `floor`). */
|
|
77
|
+
function nextUnescaped(state, slot, needle, from, floor, max) {
|
|
78
|
+
let at = nextIndex(state, slot, needle, from);
|
|
79
|
+
while (at !== -1 && at < max && isEscaped(state.src, at, floor)) {
|
|
80
|
+
at = nextIndex(state, slot, needle, at + 1);
|
|
51
81
|
}
|
|
52
|
-
return
|
|
82
|
+
return at;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findClose(state, from) {
|
|
86
|
+
const max = state.posMax;
|
|
87
|
+
const bracket = nextUnescaped(state, '_cmBr', ']', from, from, max);
|
|
88
|
+
if (bracket === -1 || bracket >= max) return -1;
|
|
89
|
+
const newline = nextUnescaped(state, '_cmNl', '\n', from, from, max);
|
|
90
|
+
if (newline !== -1 && newline < bracket) return -1; // stays on one line
|
|
91
|
+
return bracket;
|
|
53
92
|
}
|
|
54
93
|
|
|
55
94
|
function makeRule(enabled) {
|
|
@@ -63,14 +102,14 @@ function makeRule(enabled) {
|
|
|
63
102
|
const end = findClose(state, start + 2);
|
|
64
103
|
if (end === -1) return false;
|
|
65
104
|
|
|
66
|
-
const
|
|
67
|
-
const parts =
|
|
105
|
+
const src = state.src;
|
|
106
|
+
const parts = splitInline(src, start + 2, end);
|
|
68
107
|
if (!parts) return false;
|
|
69
108
|
|
|
70
109
|
const spec = parseSpec(parts.specToken);
|
|
71
110
|
if (!spec) return false;
|
|
72
111
|
|
|
73
|
-
const rest = unescape(parts.
|
|
112
|
+
const rest = unescape(src.slice(parts.restStart, end).trim());
|
|
74
113
|
|
|
75
114
|
let token;
|
|
76
115
|
if (kind === 'meter') {
|