@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 CHANGED
@@ -63,13 +63,32 @@ ChromaMark.autoRender();
63
63
  | `autoRender(options?)` | Inject the theme, then render every target on the page. |
64
64
  | `renderElement(target, opts?)` | Render one element (selector or node) in place / as a sibling. |
65
65
  | `renderAll(selector?, opts?)` | Render all matching elements. |
66
+ | `renderSrc(target, opts?)` | Fetch the element's `data-chromamark-src` file and render it in place. |
66
67
  | `injectTheme(doc?)` | Add the theme `<style>` once (idempotent). |
67
68
  | `render(src, opts?)` | ChromaMark string → HTML. |
68
69
  | `theme` | The theme CSS as a string. |
69
70
 
70
71
  Targets default to `<script type="text/chromamark">`, `template.chromamark`,
71
- `.chromamark`, and `[data-chromamark]`. Adding `data-chromamark-auto` to the
72
- loading `<script>` runs `autoRender()` automatically once the DOM is ready.
72
+ `.chromamark`, `[data-chromamark]`, and `[data-chromamark-src]`. Adding
73
+ `data-chromamark-auto` to the loading `<script>` runs `autoRender()`
74
+ automatically once the DOM is ready.
75
+
76
+ ### Load ChromaMark from an external file
77
+
78
+ Point an element at a `.cm` file with `data-chromamark-src` and the bundle
79
+ fetches and renders it — the same idea as an external script or stylesheet, so
80
+ the page itself stays lean:
81
+
82
+ ```html
83
+ <div data-chromamark-src="report.cm"></div>
84
+ <script src="https://cdn.jsdelivr.net/npm/@chromamark/renderer/dist/chromamark.min.js"
85
+ data-chromamark-auto></script>
86
+ ```
87
+
88
+ The file is fetched with `fetch()`, so the page must be served over `http(s)`
89
+ (not opened as `file://`) and same-origin/CORS rules apply. If the file can't be
90
+ loaded the element keeps its existing content and gets a `data-chromamark-error`
91
+ attribute, so a missing file degrades gracefully instead of throwing.
73
92
 
74
93
  ## Theme
75
94
 
@@ -96,6 +115,13 @@ npm test # node:test suite
96
115
  npm run build # esbuild → dist/chromamark.esm.js, dist/chromamark.min.js, dist/chromamark.css
97
116
  ```
98
117
 
118
+ ## Credits
119
+
120
+ Inline change-tracking syntax is adopted from **[CriticMarkup](http://criticmarkup.com/)**
121
+ (© 2013 Gabe Weatherhead & Erik Hess, Apache-2.0); ChromaMark's parser is an original,
122
+ independent implementation. See the
123
+ [main README](https://github.com/cjfravel-dev/ChromaMark#prior-art--credits) for full credits.
124
+
99
125
  ## License
100
126
 
101
127
  Modified MIT License with a SaaS source-availability provision — see [LICENSE.md](./LICENSE.md).
@@ -2387,12 +2387,12 @@ function escapedSplit(str) {
2387
2387
  const max = str.length;
2388
2388
  let pos = 0;
2389
2389
  let ch = str.charCodeAt(pos);
2390
- let isEscaped = false;
2390
+ let isEscaped2 = false;
2391
2391
  let lastPos = 0;
2392
2392
  let current = "";
2393
2393
  while (pos < max) {
2394
2394
  if (ch === 124) {
2395
- if (!isEscaped) {
2395
+ if (!isEscaped2) {
2396
2396
  result.push(current + str.substring(lastPos, pos));
2397
2397
  current = "";
2398
2398
  lastPos = pos + 1;
@@ -2401,7 +2401,7 @@ function escapedSplit(str) {
2401
2401
  lastPos = pos;
2402
2402
  }
2403
2403
  }
2404
- isEscaped = ch === 92;
2404
+ isEscaped2 = ch === 92;
2405
2405
  pos++;
2406
2406
  ch = str.charCodeAt(pos);
2407
2407
  }
@@ -5673,10 +5673,14 @@ var SIGILS = { "!": "pill", ".": "text", "=": "meter" };
5673
5673
  function unescape(text2) {
5674
5674
  return text2.replace(/\\([\s\S])/g, "$1");
5675
5675
  }
5676
- function splitSpec(inner) {
5677
- const match2 = inner.match(/^(\S+)(?:\s+([\s\S]*))?$/);
5678
- if (!match2) return null;
5679
- return { specToken: match2[1], rest: (match2[2] || "").trim() };
5676
+ var WS = /\s/;
5677
+ function splitInline(src, from, end) {
5678
+ let s = from;
5679
+ while (s < end && WS.test(src[s])) s++;
5680
+ let e = s;
5681
+ while (e < end && !WS.test(src[e])) e++;
5682
+ if (e === s) return null;
5683
+ return { specToken: src.slice(s, e), restStart: e };
5680
5684
  }
5681
5685
  function meterWidth(value) {
5682
5686
  let pct;
@@ -5694,18 +5698,32 @@ function meterWidth(value) {
5694
5698
  pct = Math.max(0, Math.min(100, pct));
5695
5699
  return String(+pct.toFixed(2));
5696
5700
  }
5697
- function findClose(state, from) {
5698
- const src = state.src;
5699
- for (let i = from; i < state.posMax; i++) {
5700
- const c = src.charCodeAt(i);
5701
- if (c === 92) {
5702
- i++;
5703
- continue;
5704
- }
5705
- if (c === 10) return -1;
5706
- if (c === 93) return i;
5701
+ function nextIndex(state, slot, needle, from) {
5702
+ const c = state[slot];
5703
+ if (c !== void 0 && c.from <= from && (c.at === -1 || from <= c.at)) return c.at;
5704
+ const at = state.src.indexOf(needle, from);
5705
+ state[slot] = { from, at };
5706
+ return at;
5707
+ }
5708
+ function isEscaped(src, pos, floor2) {
5709
+ let n = 0;
5710
+ for (let i = pos - 1; i >= floor2 && src.charCodeAt(i) === 92; i--) n++;
5711
+ return (n & 1) === 1;
5712
+ }
5713
+ function nextUnescaped(state, slot, needle, from, floor2, max) {
5714
+ let at = nextIndex(state, slot, needle, from);
5715
+ while (at !== -1 && at < max && isEscaped(state.src, at, floor2)) {
5716
+ at = nextIndex(state, slot, needle, at + 1);
5707
5717
  }
5708
- return -1;
5718
+ return at;
5719
+ }
5720
+ function findClose(state, from) {
5721
+ const max = state.posMax;
5722
+ const bracket = nextUnescaped(state, "_cmBr", "]", from, from, max);
5723
+ if (bracket === -1 || bracket >= max) return -1;
5724
+ const newline2 = nextUnescaped(state, "_cmNl", "\n", from, from, max);
5725
+ if (newline2 !== -1 && newline2 < bracket) return -1;
5726
+ return bracket;
5709
5727
  }
5710
5728
  function makeRule(enabled) {
5711
5729
  return function chromaInline(state, silent) {
@@ -5715,12 +5733,12 @@ function makeRule(enabled) {
5715
5733
  if (!kind || !enabled[kind]) return false;
5716
5734
  const end = findClose(state, start + 2);
5717
5735
  if (end === -1) return false;
5718
- const inner = state.src.slice(start + 2, end).trim();
5719
- const parts = inner && splitSpec(inner);
5736
+ const src = state.src;
5737
+ const parts = splitInline(src, start + 2, end);
5720
5738
  if (!parts) return false;
5721
5739
  const spec = parseSpec(parts.specToken);
5722
5740
  if (!spec) return false;
5723
- const rest = unescape(parts.rest);
5741
+ const rest = unescape(src.slice(parts.restStart, end).trim());
5724
5742
  let token;
5725
5743
  if (kind === "meter") {
5726
5744
  if (!rest) return false;
@@ -5780,6 +5798,14 @@ var KINDS = {
5780
5798
  "==": { kind: "mark", close: "==}" },
5781
5799
  ">>": { kind: "comment", close: "<<}" }
5782
5800
  };
5801
+ function findClose2(state, needle, from) {
5802
+ const slot = "_cmCritic" + needle;
5803
+ const c = state[slot];
5804
+ if (c !== void 0 && c.from <= from && (c.at === -1 || from <= c.at)) return c.at;
5805
+ const at = state.src.indexOf(needle, from);
5806
+ state[slot] = { from, at };
5807
+ return at;
5808
+ }
5783
5809
  function criticRule(state, silent) {
5784
5810
  const start = state.pos;
5785
5811
  const src = state.src;
@@ -5787,7 +5813,7 @@ function criticRule(state, silent) {
5787
5813
  const spec = KINDS[src.slice(start + 1, start + 3)];
5788
5814
  if (!spec) return false;
5789
5815
  const contentStart = start + 3;
5790
- const closeIdx = src.indexOf(spec.close, contentStart);
5816
+ const closeIdx = findClose2(state, spec.close, contentStart);
5791
5817
  if (closeIdx === -1 || closeIdx + spec.close.length > state.posMax) return false;
5792
5818
  const raw = src.slice(contentStart, closeIdx);
5793
5819
  if (!silent) {
@@ -6033,7 +6059,9 @@ function render(src, options = {}) {
6033
6059
  // src/browser-core.js
6034
6060
  var STYLE_ID = "chromamark-theme";
6035
6061
  var DONE_ATTR = "data-chromamark-done";
6036
- var DEFAULT_SELECTOR = 'script[type="text/chromamark"], template.chromamark, [data-chromamark], .chromamark';
6062
+ var SRC_ATTR = "data-chromamark-src";
6063
+ var ERR_ATTR = "data-chromamark-error";
6064
+ var DEFAULT_SELECTOR = 'script[type="text/chromamark"], template.chromamark, [data-chromamark], [data-chromamark-src], .chromamark';
6037
6065
  var theme = "";
6038
6066
  function configureTheme(css) {
6039
6067
  theme = css || "";
@@ -6071,20 +6099,44 @@ function sourceOf(el, tag) {
6071
6099
  function renderElement(target, options) {
6072
6100
  const el = resolve(target);
6073
6101
  if (!el || el.hasAttribute(DONE_ATTR)) return null;
6102
+ if (el.hasAttribute && el.hasAttribute(SRC_ATTR)) return renderSrc(el, options);
6103
+ const doc = el.ownerDocument || (typeof document !== "undefined" ? document : null);
6074
6104
  const tag = (el.tagName || "").toLowerCase();
6075
6105
  const html = render(dedent(sourceOf(el, tag)), options);
6076
6106
  el.setAttribute(DONE_ATTR, "");
6077
6107
  if (tag === "script" || tag === "template") {
6078
- const out = document.createElement("div");
6108
+ const out = doc.createElement("div");
6079
6109
  out.className = "chromamark-output";
6080
6110
  out.innerHTML = html;
6081
- el.parentNode.insertBefore(out, el.nextSibling);
6111
+ if (el.parentNode) el.parentNode.insertBefore(out, el.nextSibling);
6082
6112
  return out;
6083
6113
  }
6084
6114
  el.innerHTML = html;
6085
6115
  el.classList.add("chromamark-output");
6086
6116
  return el;
6087
6117
  }
6118
+ function renderSrc(target, options) {
6119
+ const el = resolve(target);
6120
+ if (!el || el.hasAttribute(DONE_ATTR)) return Promise.resolve(null);
6121
+ const url = el.getAttribute(SRC_ATTR);
6122
+ if (!url) return Promise.resolve(null);
6123
+ el.setAttribute(DONE_ATTR, "");
6124
+ const view = el.ownerDocument && el.ownerDocument.defaultView;
6125
+ const doFetch = view && view.fetch || (typeof fetch !== "undefined" ? fetch : null);
6126
+ const fail = (message) => {
6127
+ el.setAttribute(ERR_ATTR, message);
6128
+ return null;
6129
+ };
6130
+ if (!doFetch) return Promise.resolve(fail("ChromaMark: fetch is unavailable"));
6131
+ return Promise.resolve().then(() => doFetch(url)).then((res) => {
6132
+ if (!res || !res.ok) throw new Error(`HTTP ${res ? res.status : "?"}`);
6133
+ return res.text();
6134
+ }).then((text2) => {
6135
+ el.innerHTML = render(text2, options);
6136
+ el.classList.add("chromamark-output");
6137
+ return el;
6138
+ }).catch((err) => fail(`ChromaMark: failed to load ${url} (${err && err.message || err})`));
6139
+ }
6088
6140
  function renderAll(selector, options) {
6089
6141
  const nodes = document.querySelectorAll(selector || DEFAULT_SELECTOR);
6090
6142
  return Array.from(nodes).map((el) => renderElement(el, options));
@@ -6097,6 +6149,7 @@ var ChromaMark = {
6097
6149
  render: render2,
6098
6150
  renderElement,
6099
6151
  renderAll,
6152
+ renderSrc,
6100
6153
  injectTheme,
6101
6154
  autoRender,
6102
6155
  createRenderer,
@@ -6128,5 +6181,6 @@ export {
6128
6181
  render2 as render,
6129
6182
  renderAll,
6130
6183
  renderElement,
6184
+ renderSrc,
6131
6185
  theme
6132
6186
  };