orz-markdown 1.1.0 → 1.2.1

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
@@ -37,7 +37,7 @@ document.body.innerHTML = `<article class="markdown-body">${html}</article>`;
37
37
 
38
38
  ## Browser Runtime
39
39
 
40
- Some rendered features expect a small browser runtime layer after the HTML is mounted. This currently includes QR-code expand/collapse behavior and is designed so more client-side enhancements can share the same entry point.
40
+ Some rendered features expect a small browser runtime layer after the HTML is mounted. This includes QR-code expand/collapse behavior and **copy-as-Markdown**, and is designed so more client-side enhancements can share the same entry point.
41
41
 
42
42
  ```javascript
43
43
  import { getBrowserRuntimeScript } from 'orz-markdown/runtime';
@@ -49,6 +49,20 @@ document.body.appendChild(runtimeScript);
49
49
 
50
50
  If your app controls initialization directly, the runtime also exposes `window.OrzMarkdownRuntime.init(root)` and `window.OrzMarkdownRuntime.initQrCodes(root)`.
51
51
 
52
+ ### Copy as Markdown
53
+
54
+ Once the runtime is loaded, selecting rendered content and copying it (Cmd/Ctrl-C) places **Markdown source** on the clipboard instead of HTML. A built-in DOM→Markdown walker reconstructs headings, emphasis, inline code, links, `==mark==`/`++ins++`/`~~del~~`/`~sub~`/`^sup^`, bullet/ordered/nested lists, task lists, tables (with alignment), blockquotes, fenced code (with language), images, and inline/display math.
55
+
56
+ It only transforms selections inside an element with class `markdown-body` (wrap your rendered output in one, e.g. `<article class="markdown-body">…</article>`, or mark a container with `data-orz-copy`), and never touches selections inside `<input>`, `<textarea>`, or `contenteditable` regions.
57
+
58
+ Generated constructs that lose their source after client-side rendering — `mermaid`, `smiles`, `qrcode`, `youtube` — carry a `data-md` attribute that the walker emits verbatim. As a result a copied table of contents yields its heading links (not `{{toc 2,3}}`) and a copied QR code yields `{{qr ...}}` (not its SVG). **Do not strip `data-md` attributes** if you post-process the HTML.
59
+
60
+ You can also convert a node programmatically:
61
+
62
+ ```javascript
63
+ const markdown = window.OrzMarkdownRuntime.elementToMarkdown(someElement);
64
+ ```
65
+
52
66
  ## Themes
53
67
 
54
68
  We provide multiple ready-to-use CSS themes for rendering the output! You do not need to style the custom plugins from scratch.
@@ -7,12 +7,25 @@ function escapeHtml(str) {
7
7
  .replace(/</g, '&lt;')
8
8
  .replace(/>/g, '&gt;');
9
9
  }
10
+ // Encode a value for a double-quoted HTML attribute, preserving newlines (as
11
+ // &#10;) so multi-line source survives in `data-md`.
12
+ function escapeAttr(str) {
13
+ return str
14
+ .replace(/&/g, '&amp;')
15
+ .replace(/"/g, '&quot;')
16
+ .replace(/</g, '&lt;')
17
+ .replace(/>/g, '&gt;')
18
+ .replace(/\n/g, '&#10;');
19
+ }
10
20
  (0, registry_js_1.register)({
11
21
  type: 'block',
12
22
  aliases: ['mermaid', 'mm'],
13
23
  render(_args, body, _env) {
14
24
  const content = body?.trim() ?? '';
15
- return `<div class="mermaid">${escapeHtml(content)}</div>\n`;
25
+ // `data-md` lets copy-as-markdown recover the source after client-side
26
+ // mermaid rendering replaces the div contents with an <svg>.
27
+ const directive = `{{mermaid\n${content}\n}}`;
28
+ return `<div class="mermaid" data-md="${escapeAttr(directive)}">${escapeHtml(content)}</div>\n`;
16
29
  },
17
30
  });
18
31
  //# sourceMappingURL=mermaid.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mermaid.js","sourceRoot":"","sources":["../../src/plugins/mermaid.ts"],"names":[],"mappings":";;AAAA,gDAA0C;AAE1C,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,IAAA,sBAAQ,EAAC;IACP,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC;IAC1B,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI;QACtB,MAAM,OAAO,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACnC,OAAO,wBAAwB,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC;IAC/D,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"mermaid.js","sourceRoot":"","sources":["../../src/plugins/mermaid.ts"],"names":[],"mappings":";;AAAA,gDAA0C;AAE1C,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,6EAA6E;AAC7E,qDAAqD;AACrD,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;AAC7B,CAAC;AAED,IAAA,sBAAQ,EAAC;IACP,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC;IAC1B,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI;QACtB,MAAM,OAAO,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACnC,uEAAuE;QACvE,6DAA6D;QAC7D,MAAM,SAAS,GAAG,cAAc,OAAO,MAAM,CAAC;QAC9C,OAAO,iCAAiC,UAAU,CAAC,SAAS,CAAC,KAAK,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC;IAClG,CAAC;CACF,CAAC,CAAC"}
@@ -5,6 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const qrcode_svg_1 = __importDefault(require("qrcode-svg"));
7
7
  const registry_js_1 = require("../registry.js");
8
+ function escapeAttr(str) {
9
+ return str
10
+ .replace(/&/g, '&amp;')
11
+ .replace(/"/g, '&quot;')
12
+ .replace(/</g, '&lt;')
13
+ .replace(/>/g, '&gt;');
14
+ }
8
15
  (0, registry_js_1.register)({
9
16
  type: 'inline',
10
17
  aliases: ['qr', 'qrcode'],
@@ -15,7 +22,10 @@ const registry_js_1 = require("../registry.js");
15
22
  const size = 96;
16
23
  const rawSvg = new qrcode_svg_1.default({ content, width: size, height: size, padding: 0, color: '#000000', background: '#ffffff' }).svg();
17
24
  const svg = rawSvg.replace('<svg ', `<svg viewBox="0 0 ${size} ${size}" preserveAspectRatio="xMidYMid meet" `);
18
- return `<span class="qrcode" role="button" tabindex="0" aria-label="Expand QR code" aria-expanded="false"><span class="qrcode__icon" aria-hidden="true">⤢</span>${svg}</span>`;
25
+ // `data-md` carries the source: the QR content is otherwise encoded only in
26
+ // the SVG modules and cannot be recovered for copy-as-markdown.
27
+ const directive = escapeAttr(`{{qr ${content}}}`);
28
+ return `<span class="qrcode" data-md="${directive}" role="button" tabindex="0" aria-label="Expand QR code" aria-expanded="false"><span class="qrcode__icon" aria-hidden="true">⤢</span>${svg}</span>`;
19
29
  },
20
30
  });
21
31
  //# sourceMappingURL=qrcode.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"qrcode.js","sourceRoot":"","sources":["../../src/plugins/qrcode.ts"],"names":[],"mappings":";;;;;AAAA,4DAAgC;AAChC,gDAA0C;AAE1C,IAAA,sBAAQ,EAAC;IACP,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC;IACzB,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI;QACtB,MAAM,OAAO,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC;QAC7B,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,IAAI,oBAAM,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;QAC7H,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CACxB,OAAO,EACP,qBAAqB,IAAI,IAAI,IAAI,wCAAwC,CAC1E,CAAC;QACF,OAAO,2JAA2J,GAAG,SAAS,CAAC;IACjL,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"qrcode.js","sourceRoot":"","sources":["../../src/plugins/qrcode.ts"],"names":[],"mappings":";;;;;AAAA,4DAAgC;AAChC,gDAA0C;AAE1C,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,IAAA,sBAAQ,EAAC;IACP,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC;IACzB,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI;QACtB,MAAM,OAAO,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC;QAC7B,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,IAAI,oBAAM,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;QAC7H,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CACxB,OAAO,EACP,qBAAqB,IAAI,IAAI,IAAI,wCAAwC,CAC1E,CAAC;QACF,4EAA4E;QAC5E,gEAAgE;QAChE,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,OAAO,IAAI,CAAC,CAAC;QAClD,OAAO,iCAAiC,SAAS,wIAAwI,GAAG,SAAS,CAAC;IACxM,CAAC;CACF,CAAC,CAAC"}
@@ -13,7 +13,10 @@ function escapeAttr(str) {
13
13
  aliases: ['smiles', 'sm'],
14
14
  render(_args, body, _env) {
15
15
  const smiles = body?.trim() ?? '';
16
- return `<div class="smiles-render">\n <canvas data-smiles="${escapeAttr(smiles)}" width="250" height="180"></canvas>\n</div>\n`;
16
+ // `data-md` lets copy-as-markdown recover the source after smiles-drawer
17
+ // paints the <canvas> (which has no recoverable text content).
18
+ const directive = escapeAttr(`{{smiles ${smiles}}}`);
19
+ return `<div class="smiles-render" data-md="${directive}">\n <canvas data-smiles="${escapeAttr(smiles)}" width="250" height="180"></canvas>\n</div>\n`;
17
20
  },
18
21
  });
19
22
  //# sourceMappingURL=smiles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"smiles.js","sourceRoot":"","sources":["../../src/plugins/smiles.ts"],"names":[],"mappings":";;AAAA,gDAA0C;AAE1C,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,IAAA,sBAAQ,EAAC;IACP,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC;IACzB,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI;QACtB,MAAM,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAClC,OAAO,uDAAuD,UAAU,CAAC,MAAM,CAAC,gDAAgD,CAAC;IACnI,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"smiles.js","sourceRoot":"","sources":["../../src/plugins/smiles.ts"],"names":[],"mappings":";;AAAA,gDAA0C;AAE1C,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,IAAA,sBAAQ,EAAC;IACP,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC;IACzB,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI;QACtB,MAAM,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAClC,yEAAyE;QACzE,+DAA+D;QAC/D,MAAM,SAAS,GAAG,UAAU,CAAC,YAAY,MAAM,IAAI,CAAC,CAAC;QACrD,OAAO,uCAAuC,SAAS,8BAA8B,UAAU,CAAC,MAAM,CAAC,gDAAgD,CAAC;IAC1J,CAAC;CACF,CAAC,CAAC"}
@@ -1,6 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const registry_js_1 = require("../registry.js");
4
+ function escapeAttr(str) {
5
+ return str
6
+ .replace(/&/g, '&amp;')
7
+ .replace(/"/g, '&quot;')
8
+ .replace(/</g, '&lt;')
9
+ .replace(/>/g, '&gt;');
10
+ }
4
11
  (0, registry_js_1.register)({
5
12
  type: 'block',
6
13
  aliases: ['youtube', 'yt'],
@@ -8,7 +15,8 @@ const registry_js_1 = require("../registry.js");
8
15
  const id = body?.trim();
9
16
  if (!id)
10
17
  return '';
11
- return (`<div class="youtube-embed">\n` +
18
+ const directive = escapeAttr(`{{youtube ${id}}}`);
19
+ return (`<div class="youtube-embed" data-md="${directive}">\n` +
12
20
  ` <iframe src="https://www.youtube.com/embed/${id}"\n` +
13
21
  ` allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"\n` +
14
22
  ` referrerpolicy="strict-origin-when-cross-origin"\n` +
@@ -1 +1 @@
1
- {"version":3,"file":"youtube.js","sourceRoot":"","sources":["../../src/plugins/youtube.ts"],"names":[],"mappings":";;AAAA,gDAA0C;AAE1C,IAAA,sBAAQ,EAAC;IACP,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC;IAC1B,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI;QACtB,MAAM,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,EAAE;YAAE,OAAO,EAAE,CAAC;QACnB,OAAO,CACL,+BAA+B;YAC/B,gDAAgD,EAAE,KAAK;YACvD,mHAAmH;YACnH,wDAAwD;YACxD,iCAAiC;YACjC,UAAU,CACX,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"youtube.js","sourceRoot":"","sources":["../../src/plugins/youtube.ts"],"names":[],"mappings":";;AAAA,gDAA0C;AAE1C,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,IAAA,sBAAQ,EAAC;IACP,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC;IAC1B,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI;QACtB,MAAM,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,EAAE;YAAE,OAAO,EAAE,CAAC;QACnB,MAAM,SAAS,GAAG,UAAU,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;QAClD,OAAO,CACL,uCAAuC,SAAS,MAAM;YACtD,gDAAgD,EAAE,KAAK;YACvD,mHAAmH;YACnH,wDAAwD;YACxD,iCAAiC;YACjC,UAAU,CACX,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,QAuHhC,CAAC;AAEF,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,QA8chC,CAAC;AAEF,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD"}
package/dist/runtime.js CHANGED
@@ -86,6 +86,345 @@ exports.browserRuntimeScript = String.raw `
86
86
  initQrCodes(root || global.document);
87
87
  }
88
88
 
89
+ // ---- copy-as-markdown: DOM -> Markdown walker ---------------------------
90
+ // Converts a selected DOM fragment back to Markdown so that copying rendered
91
+ // content yields source, not HTML. Standard constructs are reconstructed from
92
+ // their tags; generated constructs (math, mermaid, smiles, qr, youtube) carry
93
+ // a [data-md] breadcrumb that is emitted verbatim. Written without template
94
+ // literals/backticks because this whole file is a String.raw template.
95
+ var BT = String.fromCharCode(96);
96
+ var FENCE = BT + BT + BT;
97
+
98
+ function rt_repeat(s, n) {
99
+ var out = '';
100
+ for (var i = 0; i < n; i++) out += s;
101
+ return out;
102
+ }
103
+
104
+ function rt_attr(node, name) {
105
+ return node && node.getAttribute ? node.getAttribute(name) : null;
106
+ }
107
+
108
+ function rt_isBlockElement(node) {
109
+ if (!node || node.nodeType !== 1) return false;
110
+ if (rt_attr(node, 'data-md') != null) return true;
111
+ var tag = node.tagName.toLowerCase();
112
+ if (/^(p|div|section|article|figure|h[1-6]|ul|ol|li|blockquote|pre|table|thead|tbody|tr|hr|details)$/.test(tag)) return true;
113
+ if (node.classList && (node.classList.contains('katex-display') || node.classList.contains('mermaid'))) return true;
114
+ return false;
115
+ }
116
+
117
+ function rt_katex(node, display) {
118
+ var ann = node.querySelector ? node.querySelector('annotation[encoding="application/x-tex"]') : null;
119
+ var tex = (ann ? ann.textContent : node.textContent) || '';
120
+ tex = tex.replace(/^\s+|\s+$/g, '');
121
+ if (display) return '$$\n' + tex + '\n$$';
122
+ return '$' + tex + '$';
123
+ }
124
+
125
+ function rt_img(node) {
126
+ var alt = rt_attr(node, 'alt') || '';
127
+ var src = rt_attr(node, 'src') || '';
128
+ var title = rt_attr(node, 'title');
129
+ return '![' + alt + '](' + src + (title ? ' "' + title + '"' : '') + ')';
130
+ }
131
+
132
+ function rt_inlineChildren(node) {
133
+ var out = '';
134
+ var kids = node.childNodes;
135
+ for (var i = 0; i < kids.length; i++) out += rt_inlineNode(kids[i]);
136
+ return out;
137
+ }
138
+
139
+ function rt_inlineNode(node) {
140
+ if (node.nodeType === 3) return node.nodeValue.replace(/\s+/g, ' ');
141
+ if (node.nodeType !== 1) return '';
142
+ var dm = rt_attr(node, 'data-md');
143
+ if (dm != null) return dm;
144
+ if (node.classList && node.classList.contains('katex')) return rt_katex(node, false);
145
+ var tag = node.tagName.toLowerCase();
146
+ switch (tag) {
147
+ case 'strong': case 'b': return '**' + rt_inlineChildren(node) + '**';
148
+ case 'em': case 'i': return '*' + rt_inlineChildren(node) + '*';
149
+ case 'code': return BT + (node.textContent || '') + BT;
150
+ case 'a':
151
+ var href = rt_attr(node, 'href') || '';
152
+ var title = rt_attr(node, 'title');
153
+ return '[' + rt_inlineChildren(node) + '](' + href + (title ? ' "' + title + '"' : '') + ')';
154
+ case 'del': case 's': return '~~' + rt_inlineChildren(node) + '~~';
155
+ case 'ins': return '++' + rt_inlineChildren(node) + '++';
156
+ case 'mark': return '==' + rt_inlineChildren(node) + '==';
157
+ case 'sub': return '~' + rt_inlineChildren(node) + '~';
158
+ case 'sup': return '^' + rt_inlineChildren(node) + '^';
159
+ case 'br': return ' \n';
160
+ case 'img': return rt_img(node);
161
+ case 'input': return '';
162
+ default: return rt_inlineChildren(node);
163
+ }
164
+ }
165
+
166
+ function rt_align(cell) {
167
+ var style = (rt_attr(cell, 'style') || '').toLowerCase();
168
+ if (style.indexOf('center') !== -1) return 'center';
169
+ if (style.indexOf('right') !== -1) return 'right';
170
+ if (style.indexOf('left') !== -1) return 'left';
171
+ return '';
172
+ }
173
+
174
+ function rt_sep(align) {
175
+ if (align === 'center') return ':---:';
176
+ if (align === 'right') return '---:';
177
+ if (align === 'left') return ':---';
178
+ return '---';
179
+ }
180
+
181
+ function rt_tableRows(headRow, bodyRows) {
182
+ var rows = [];
183
+ var header = [];
184
+ var aligns = [];
185
+ if (headRow) {
186
+ var hc = headRow.children;
187
+ for (var i = 0; i < hc.length; i++) { header.push(rt_inlineChildren(hc[i]).trim()); aligns.push(rt_align(hc[i])); }
188
+ }
189
+ rows.push('| ' + header.join(' | ') + ' |');
190
+ var seps = [];
191
+ for (var s = 0; s < header.length; s++) seps.push(rt_sep(aligns[s]));
192
+ rows.push('| ' + seps.join(' | ') + ' |');
193
+ for (var r = 0; r < bodyRows.length; r++) {
194
+ var cells = [];
195
+ var tds = bodyRows[r].children;
196
+ for (var c = 0; c < tds.length; c++) cells.push(rt_inlineChildren(tds[c]).trim());
197
+ rows.push('| ' + cells.join(' | ') + ' |');
198
+ }
199
+ return rows.join('\n');
200
+ }
201
+
202
+ function rt_table(tbl) {
203
+ var headRow = tbl.querySelector ? tbl.querySelector('thead tr') : null;
204
+ var bodyRows = tbl.querySelectorAll ? tbl.querySelectorAll('tbody tr') : [];
205
+ return rt_tableRows(headRow, bodyRows);
206
+ }
207
+
208
+ // Reconstruct a table from bare table-internal nodes (thead/tbody/tr), e.g.
209
+ // when a selection inside a table loses its <table> wrapper.
210
+ function rt_tableFromParts(parts) {
211
+ var headRow = null;
212
+ var bodyRows = [];
213
+ for (var i = 0; i < parts.length; i++) {
214
+ var p = parts[i];
215
+ var tag = p.tagName.toLowerCase();
216
+ if (tag === 'thead') { var tr = p.querySelector ? p.querySelector('tr') : null; if (tr && !headRow) headRow = tr; }
217
+ else if (tag === 'tbody') { var trs = p.querySelectorAll ? p.querySelectorAll('tr') : []; for (var j = 0; j < trs.length; j++) bodyRows.push(trs[j]); }
218
+ else if (tag === 'tr') { if (!headRow) headRow = p; else bodyRows.push(p); }
219
+ }
220
+ return rt_tableRows(headRow, bodyRows);
221
+ }
222
+
223
+ function rt_codeBlock(pre) {
224
+ var code = pre.querySelector ? (pre.querySelector('code') || pre) : pre;
225
+ var lang = '';
226
+ var cls = rt_attr(code, 'class') || '';
227
+ var m = cls.match(/language-([A-Za-z0-9_+#-]+)/);
228
+ if (m) lang = m[1];
229
+ var text = (code.textContent || '').replace(/\n$/, '');
230
+ return FENCE + lang + '\n' + text + '\n' + FENCE;
231
+ }
232
+
233
+ function rt_list(node, ordered, depth) {
234
+ var items = [];
235
+ var idx = 1;
236
+ var kids = node.childNodes;
237
+ for (var i = 0; i < kids.length; i++) {
238
+ var li = kids[i];
239
+ if (li.nodeType !== 1 || li.tagName.toLowerCase() !== 'li') continue;
240
+ items.push(rt_listItem(li, ordered, idx, depth));
241
+ idx++;
242
+ }
243
+ return items.join('\n');
244
+ }
245
+
246
+ function rt_listItem(li, ordered, idx, depth) {
247
+ var indent = rt_repeat(' ', depth);
248
+ var marker = ordered ? (idx + '. ') : '- ';
249
+ var task = '';
250
+ var cb = li.querySelector ? li.querySelector('input[type="checkbox"]') : null;
251
+ if (cb) task = (cb.checked || rt_attr(cb, 'checked') != null) ? '[x] ' : '[ ] ';
252
+ var inlineParts = '';
253
+ var nested = '';
254
+ var kids = li.childNodes;
255
+ for (var i = 0; i < kids.length; i++) {
256
+ var c = kids[i];
257
+ if (c.nodeType === 1 && /^(ul|ol)$/.test(c.tagName.toLowerCase())) {
258
+ nested += '\n' + rt_list(c, c.tagName.toLowerCase() === 'ol', depth + 1);
259
+ } else {
260
+ inlineParts += rt_inlineNode(c);
261
+ }
262
+ }
263
+ inlineParts = inlineParts.replace(/\s+/g, ' ').trim();
264
+ return indent + marker + task + inlineParts + nested;
265
+ }
266
+
267
+ function rt_blockquote(node) {
268
+ var inner = rt_blockChildren(node);
269
+ var lines = inner.split('\n');
270
+ var out = [];
271
+ for (var i = 0; i < lines.length; i++) out.push(lines[i] ? '> ' + lines[i] : '>');
272
+ return out.join('\n');
273
+ }
274
+
275
+ function rt_blockChildren(node) {
276
+ var parts = [];
277
+ var kids = node.childNodes;
278
+ var i = 0;
279
+ while (i < kids.length) {
280
+ var k = kids[i];
281
+ // A selection can contain bare <li> elements (when the user selects a
282
+ // list's contents rather than the <ul>/<ol> wrapper). Group a run of them
283
+ // into a bullet list instead of emitting each as a separate block.
284
+ if (k.nodeType === 1 && k.tagName.toLowerCase() === 'li') {
285
+ var lis = [];
286
+ while (i < kids.length) {
287
+ var kk = kids[i];
288
+ if (kk.nodeType === 1 && kk.tagName.toLowerCase() === 'li') { lis.push(kk); i++; }
289
+ else if (kk.nodeType === 3 && !(kk.nodeValue || '').trim()) { i++; }
290
+ else break;
291
+ }
292
+ var items = [];
293
+ for (var j = 0; j < lis.length; j++) items.push(rt_listItem(lis[j], false, j + 1, 0));
294
+ parts.push(items.join('\n'));
295
+ continue;
296
+ }
297
+ // Bare table-internal nodes (selection inside a table that lost its
298
+ // <table> wrapper) — reconstruct a Markdown table.
299
+ if (k.nodeType === 1 && /^(thead|tbody|tr)$/.test(k.tagName.toLowerCase())) {
300
+ var trows = [];
301
+ while (i < kids.length) {
302
+ var tk = kids[i];
303
+ if (tk.nodeType === 1 && /^(thead|tbody|tr)$/.test(tk.tagName.toLowerCase())) { trows.push(tk); i++; }
304
+ else if (tk.nodeType === 3 && !(tk.nodeValue || '').trim()) { i++; }
305
+ else break;
306
+ }
307
+ parts.push(rt_tableFromParts(trows));
308
+ continue;
309
+ }
310
+ var s = rt_blockNode(k);
311
+ if (s && s.replace(/\s/g, '') !== '') parts.push(s.replace(/\s+$/, ''));
312
+ i++;
313
+ }
314
+ return parts.join('\n\n');
315
+ }
316
+
317
+ function rt_blockNode(node) {
318
+ if (node.nodeType === 3) {
319
+ var t = node.nodeValue;
320
+ return (t && t.trim()) ? t.replace(/\s+/g, ' ').trim() : '';
321
+ }
322
+ if (node.nodeType !== 1) return '';
323
+ var dm = rt_attr(node, 'data-md');
324
+ if (dm != null) return dm;
325
+ if (node.classList) {
326
+ if (node.classList.contains('katex-display')) {
327
+ var k = node.querySelector ? node.querySelector('.katex') : null;
328
+ return rt_katex(k || node, true);
329
+ }
330
+ if (node.classList.contains('mermaid')) return '{{mermaid\n' + (node.textContent || '').trim() + '\n}}';
331
+ }
332
+ var tag = node.tagName.toLowerCase();
333
+ var hm = tag.match(/^h([1-6])$/);
334
+ if (hm) return rt_repeat('#', parseInt(hm[1], 10)) + ' ' + rt_inlineChildren(node).trim();
335
+ switch (tag) {
336
+ case 'p': return rt_inlineChildren(node).trim();
337
+ case 'ul': return rt_list(node, false, 0);
338
+ case 'ol': return rt_list(node, true, 0);
339
+ case 'pre': return rt_codeBlock(node);
340
+ case 'blockquote': return rt_blockquote(node);
341
+ case 'table': return rt_table(node);
342
+ case 'hr': return '---';
343
+ case 'li': return rt_inlineChildren(node).trim();
344
+ case 'figure': case 'div': case 'section': case 'article': case 'details': case 'summary':
345
+ return rt_blockChildren(node);
346
+ default: return rt_inlineNode(node).trim();
347
+ }
348
+ }
349
+
350
+ function elementToMarkdown(root) {
351
+ if (!root) return '';
352
+ var hasBlock = false;
353
+ var kids = root.childNodes || [];
354
+ for (var i = 0; i < kids.length; i++) {
355
+ if (rt_isBlockElement(kids[i])) { hasBlock = true; break; }
356
+ }
357
+ var out = hasBlock ? rt_blockChildren(root) : rt_inlineChildren(root);
358
+ return out.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').replace(/^\s+|\s+$/g, '');
359
+ }
360
+
361
+ function rt_isEditable(node) {
362
+ while (node) {
363
+ if (node.nodeType === 1) {
364
+ var tag = node.tagName.toLowerCase();
365
+ if (tag === 'input' || tag === 'textarea') return true;
366
+ if (node.isContentEditable) return true;
367
+ }
368
+ node = node.parentNode;
369
+ }
370
+ return false;
371
+ }
372
+
373
+ function rt_isCopyRoot(node) {
374
+ return node.nodeType === 1 && node.classList &&
375
+ (node.classList.contains('markdown-body') || rt_attr(node, 'data-orz-copy') != null);
376
+ }
377
+ function rt_withinCopyRoot(node) {
378
+ while (node) {
379
+ if (rt_isCopyRoot(node)) return true;
380
+ node = node.parentNode;
381
+ }
382
+ return false;
383
+ }
384
+
385
+ // When a selection sits entirely within one table/blockquote/pre, copy that
386
+ // whole block: a partial table/quote/code fragment isn't valid Markdown, and
387
+ // browsers often clone such selections without the wrapping element.
388
+ function rt_promotableBlock(node) {
389
+ while (node && !rt_isCopyRoot(node)) {
390
+ if (node.nodeType === 1) {
391
+ var tag = node.tagName.toLowerCase();
392
+ if (tag === 'table' || tag === 'blockquote' || tag === 'pre') return node;
393
+ }
394
+ node = node.parentNode;
395
+ }
396
+ return null;
397
+ }
398
+
399
+ function onCopy(event) {
400
+ if (!global.document) return;
401
+ var sel = global.getSelection ? global.getSelection()
402
+ : (global.document.getSelection ? global.document.getSelection() : null);
403
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
404
+ if (rt_isEditable(sel.anchorNode) || rt_isEditable(sel.focusNode)) return;
405
+ if (!rt_withinCopyRoot(sel.anchorNode) && !rt_withinCopyRoot(sel.focusNode)) return;
406
+
407
+ var md;
408
+ var range0 = sel.getRangeAt(0);
409
+ var common = range0.commonAncestorContainer;
410
+ var promoted = rt_promotableBlock(common.nodeType === 1 ? common : common.parentNode);
411
+ if (promoted && sel.rangeCount === 1) {
412
+ var pwrap = global.document.createElement('div');
413
+ pwrap.appendChild(promoted.cloneNode(true));
414
+ md = elementToMarkdown(pwrap);
415
+ } else {
416
+ var container = global.document.createElement('div');
417
+ for (var i = 0; i < sel.rangeCount; i++) container.appendChild(sel.getRangeAt(i).cloneContents());
418
+ md = elementToMarkdown(container);
419
+ }
420
+ if (!md) return;
421
+ var cd = event.clipboardData || global.clipboardData;
422
+ if (cd && cd.setData) {
423
+ cd.setData('text/plain', md);
424
+ if (event.preventDefault) event.preventDefault();
425
+ }
426
+ }
427
+
89
428
  function bindGlobalHandlers() {
90
429
  if (!global.document || global.__orzMarkdownRuntimeBound) return;
91
430
  global.__orzMarkdownRuntimeBound = true;
@@ -101,12 +440,16 @@ exports.browserRuntimeScript = String.raw `
101
440
  collapseQr();
102
441
  }
103
442
  });
443
+
444
+ // Copy rendered selections as Markdown source.
445
+ global.document.addEventListener('copy', onCopy);
104
446
  }
105
447
 
106
448
  global.OrzMarkdownRuntime = Object.assign({}, global.OrzMarkdownRuntime, {
107
449
  init: init,
108
450
  initQrCodes: initQrCodes,
109
451
  collapseQr: collapseQr,
452
+ elementToMarkdown: elementToMarkdown,
110
453
  });
111
454
 
112
455
  bindGlobalHandlers();
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":";;;AAyHA,0DAEC;AA3HY,QAAA,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuH7C,CAAC;AAEF,SAAgB,uBAAuB;IACrC,OAAO,4BAAoB,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":";;;AAgdA,0DAEC;AAldY,QAAA,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8c7C,CAAC;AAEF,SAAgB,uBAAuB;IACrC,OAAO,4BAAoB,CAAC;AAC9B,CAAC"}
@@ -67,6 +67,10 @@ document.body.appendChild(script);
67
67
  // or call directly: window.OrzMarkdownRuntime.init(rootElement)
68
68
  ```
69
69
 
70
+ The runtime also provides **copy-as-Markdown**: with it loaded, copying a selection inside `.markdown-body` puts Markdown source on the clipboard, not HTML (tables, lists, math, code, etc. are reconstructed). It skips selections inside `<input>`/`<textarea>`/`contenteditable`. Convert a node directly with `window.OrzMarkdownRuntime.elementToMarkdown(node)`.
71
+
72
+ > **Do not strip `data-md` attributes.** `mermaid`, `smiles`, `qrcode`, and `youtube` output carry a `data-md` breadcrumb so copy recovers their source after client-side rendering (e.g. a copied QR yields `{{qr ...}}`, not its SVG). Preserve these attributes if you post-process the HTML.
73
+
70
74
  ---
71
75
 
72
76
  ## Themes
@@ -94,8 +94,9 @@ if (typeof SmilesDrawer !== 'undefined') {
94
94
  })();
95
95
  </script>
96
96
 
97
- <!-- QR code runtime — click to expand {{qrcode}} / {{qr}} overlays.
98
- Provides window.OrzMarkdownRuntime.init(root) for programmatic use. -->
97
+ <!-- orz-markdown runtime — QR expand/collapse overlays and copy-as-Markdown
98
+ (copying a selection inside .markdown-body yields Markdown source).
99
+ Provides window.OrzMarkdownRuntime.init(root) / .elementToMarkdown(node). -->
99
100
  <script>
100
101
  (function (global) {
101
102
  var expandedQr = null;
@@ -103,22 +104,38 @@ if (typeof SmilesDrawer !== 'undefined') {
103
104
 
104
105
  function collapseQr() {
105
106
  if (!expandedQr) return;
107
+
106
108
  if (expandedSourceQr) {
107
109
  expandedSourceQr.classList.remove('is-expanded');
108
110
  expandedSourceQr.setAttribute('aria-expanded', 'false');
109
111
  }
110
- if (expandedQr.parentNode) expandedQr.parentNode.removeChild(expandedQr);
112
+
113
+ if (expandedQr.parentNode) {
114
+ expandedQr.parentNode.removeChild(expandedQr);
115
+ }
116
+
117
+ if (global.document && global.document.body) {
118
+ global.document.body.style.overflow = '';
119
+ }
111
120
  expandedQr = null;
112
121
  expandedSourceQr = null;
113
- if (global.document && global.document.body) global.document.body.style.overflow = '';
114
122
  }
115
123
 
116
124
  function toggleQr(node) {
117
- if (expandedSourceQr === node) { collapseQr(); return; }
118
- if (expandedQr) collapseQr();
125
+ if (expandedSourceQr === node) {
126
+ collapseQr();
127
+ return;
128
+ }
129
+
130
+ collapseQr();
131
+ if (!global.document || !global.document.body) {
132
+ return;
133
+ }
134
+
119
135
  expandedSourceQr = node;
120
- node.classList.add('is-expanded');
121
- node.setAttribute('aria-expanded', 'true');
136
+ expandedSourceQr.classList.add('is-expanded');
137
+ expandedSourceQr.setAttribute('aria-expanded', 'true');
138
+
122
139
  expandedQr = node.cloneNode(true);
123
140
  expandedQr.classList.add('qrcode-overlay', 'is-expanded');
124
141
  expandedQr.setAttribute('aria-hidden', 'true');
@@ -129,18 +146,21 @@ if (typeof SmilesDrawer !== 'undefined') {
129
146
  event.stopPropagation();
130
147
  collapseQr();
131
148
  });
149
+
132
150
  global.document.body.appendChild(expandedQr);
133
- if (global.document && global.document.body) global.document.body.style.overflow = 'hidden';
151
+
152
+ if (global.document && global.document.body) {
153
+ global.document.body.style.overflow = 'hidden';
154
+ }
134
155
  }
135
156
 
136
157
  function initQrCodes(root) {
137
158
  if (!root || typeof root.querySelectorAll !== 'function') return;
159
+
138
160
  Array.prototype.slice.call(root.querySelectorAll('.qrcode')).forEach(function (node) {
139
161
  if (node.getAttribute('data-qr-ready') === '1') return;
162
+
140
163
  node.setAttribute('data-qr-ready', '1');
141
- node.setAttribute('tabindex', '0');
142
- node.setAttribute('role', 'button');
143
- node.setAttribute('aria-expanded', 'false');
144
164
  node.addEventListener('click', function (event) {
145
165
  event.stopPropagation();
146
166
  toggleQr(node);
@@ -150,27 +170,330 @@ if (typeof SmilesDrawer !== 'undefined') {
150
170
  event.preventDefault();
151
171
  toggleQr(node);
152
172
  }
173
+ if (event.key === 'Escape') {
174
+ collapseQr();
175
+ }
153
176
  });
154
177
  });
155
178
  }
156
179
 
180
+ function init(root) {
181
+ initQrCodes(root || global.document);
182
+ }
183
+
184
+ // ---- copy-as-markdown: DOM -> Markdown walker ---------------------------
185
+ // Converts a selected DOM fragment back to Markdown so that copying rendered
186
+ // content yields source, not HTML. Standard constructs are reconstructed from
187
+ // their tags; generated constructs (math, mermaid, smiles, qr, youtube) carry
188
+ // a [data-md] breadcrumb that is emitted verbatim. Written without template
189
+ // literals/backticks because this whole file is a String.raw template.
190
+ var BT = String.fromCharCode(96);
191
+ var FENCE = BT + BT + BT;
192
+
193
+ function rt_repeat(s, n) {
194
+ var out = '';
195
+ for (var i = 0; i < n; i++) out += s;
196
+ return out;
197
+ }
198
+
199
+ function rt_attr(node, name) {
200
+ return node && node.getAttribute ? node.getAttribute(name) : null;
201
+ }
202
+
203
+ function rt_isBlockElement(node) {
204
+ if (!node || node.nodeType !== 1) return false;
205
+ if (rt_attr(node, 'data-md') != null) return true;
206
+ var tag = node.tagName.toLowerCase();
207
+ if (/^(p|div|section|article|figure|h[1-6]|ul|ol|li|blockquote|pre|table|thead|tbody|tr|hr|details)$/.test(tag)) return true;
208
+ if (node.classList && (node.classList.contains('katex-display') || node.classList.contains('mermaid'))) return true;
209
+ return false;
210
+ }
211
+
212
+ function rt_katex(node, display) {
213
+ var ann = node.querySelector ? node.querySelector('annotation[encoding="application/x-tex"]') : null;
214
+ var tex = (ann ? ann.textContent : node.textContent) || '';
215
+ tex = tex.replace(/^\s+|\s+$/g, '');
216
+ if (display) return '$$\n' + tex + '\n$$';
217
+ return '$' + tex + '$';
218
+ }
219
+
220
+ function rt_img(node) {
221
+ var alt = rt_attr(node, 'alt') || '';
222
+ var src = rt_attr(node, 'src') || '';
223
+ var title = rt_attr(node, 'title');
224
+ return '![' + alt + '](' + src + (title ? ' "' + title + '"' : '') + ')';
225
+ }
226
+
227
+ function rt_inlineChildren(node) {
228
+ var out = '';
229
+ var kids = node.childNodes;
230
+ for (var i = 0; i < kids.length; i++) out += rt_inlineNode(kids[i]);
231
+ return out;
232
+ }
233
+
234
+ function rt_inlineNode(node) {
235
+ if (node.nodeType === 3) return node.nodeValue.replace(/\s+/g, ' ');
236
+ if (node.nodeType !== 1) return '';
237
+ var dm = rt_attr(node, 'data-md');
238
+ if (dm != null) return dm;
239
+ if (node.classList && node.classList.contains('katex')) return rt_katex(node, false);
240
+ var tag = node.tagName.toLowerCase();
241
+ switch (tag) {
242
+ case 'strong': case 'b': return '**' + rt_inlineChildren(node) + '**';
243
+ case 'em': case 'i': return '*' + rt_inlineChildren(node) + '*';
244
+ case 'code': return BT + (node.textContent || '') + BT;
245
+ case 'a':
246
+ var href = rt_attr(node, 'href') || '';
247
+ var title = rt_attr(node, 'title');
248
+ return '[' + rt_inlineChildren(node) + '](' + href + (title ? ' "' + title + '"' : '') + ')';
249
+ case 'del': case 's': return '~~' + rt_inlineChildren(node) + '~~';
250
+ case 'ins': return '++' + rt_inlineChildren(node) + '++';
251
+ case 'mark': return '==' + rt_inlineChildren(node) + '==';
252
+ case 'sub': return '~' + rt_inlineChildren(node) + '~';
253
+ case 'sup': return '^' + rt_inlineChildren(node) + '^';
254
+ case 'br': return ' \n';
255
+ case 'img': return rt_img(node);
256
+ case 'input': return '';
257
+ default: return rt_inlineChildren(node);
258
+ }
259
+ }
260
+
261
+ function rt_align(cell) {
262
+ var style = (rt_attr(cell, 'style') || '').toLowerCase();
263
+ if (style.indexOf('center') !== -1) return 'center';
264
+ if (style.indexOf('right') !== -1) return 'right';
265
+ if (style.indexOf('left') !== -1) return 'left';
266
+ return '';
267
+ }
268
+
269
+ function rt_sep(align) {
270
+ if (align === 'center') return ':---:';
271
+ if (align === 'right') return '---:';
272
+ if (align === 'left') return ':---';
273
+ return '---';
274
+ }
275
+
276
+ function rt_table(tbl) {
277
+ var rows = [];
278
+ var aligns = [];
279
+ var header = [];
280
+ var headRow = tbl.querySelector ? tbl.querySelector('thead tr') : null;
281
+ if (headRow) {
282
+ var hc = headRow.children;
283
+ for (var i = 0; i < hc.length; i++) { header.push(rt_inlineChildren(hc[i]).trim()); aligns.push(rt_align(hc[i])); }
284
+ }
285
+ rows.push('| ' + header.join(' | ') + ' |');
286
+ var seps = [];
287
+ for (var s = 0; s < header.length; s++) seps.push(rt_sep(aligns[s]));
288
+ rows.push('| ' + seps.join(' | ') + ' |');
289
+ var bodyRows = tbl.querySelectorAll ? tbl.querySelectorAll('tbody tr') : [];
290
+ for (var r = 0; r < bodyRows.length; r++) {
291
+ var cells = [];
292
+ var tds = bodyRows[r].children;
293
+ for (var c = 0; c < tds.length; c++) cells.push(rt_inlineChildren(tds[c]).trim());
294
+ rows.push('| ' + cells.join(' | ') + ' |');
295
+ }
296
+ return rows.join('\n');
297
+ }
298
+
299
+ function rt_codeBlock(pre) {
300
+ var code = pre.querySelector ? (pre.querySelector('code') || pre) : pre;
301
+ var lang = '';
302
+ var cls = rt_attr(code, 'class') || '';
303
+ var m = cls.match(/language-([A-Za-z0-9_+#-]+)/);
304
+ if (m) lang = m[1];
305
+ var text = (code.textContent || '').replace(/\n$/, '');
306
+ return FENCE + lang + '\n' + text + '\n' + FENCE;
307
+ }
308
+
309
+ function rt_list(node, ordered, depth) {
310
+ var items = [];
311
+ var idx = 1;
312
+ var kids = node.childNodes;
313
+ for (var i = 0; i < kids.length; i++) {
314
+ var li = kids[i];
315
+ if (li.nodeType !== 1 || li.tagName.toLowerCase() !== 'li') continue;
316
+ items.push(rt_listItem(li, ordered, idx, depth));
317
+ idx++;
318
+ }
319
+ return items.join('\n');
320
+ }
321
+
322
+ function rt_listItem(li, ordered, idx, depth) {
323
+ var indent = rt_repeat(' ', depth);
324
+ var marker = ordered ? (idx + '. ') : '- ';
325
+ var task = '';
326
+ var cb = li.querySelector ? li.querySelector('input[type="checkbox"]') : null;
327
+ if (cb) task = (cb.checked || rt_attr(cb, 'checked') != null) ? '[x] ' : '[ ] ';
328
+ var inlineParts = '';
329
+ var nested = '';
330
+ var kids = li.childNodes;
331
+ for (var i = 0; i < kids.length; i++) {
332
+ var c = kids[i];
333
+ if (c.nodeType === 1 && /^(ul|ol)$/.test(c.tagName.toLowerCase())) {
334
+ nested += '\n' + rt_list(c, c.tagName.toLowerCase() === 'ol', depth + 1);
335
+ } else {
336
+ inlineParts += rt_inlineNode(c);
337
+ }
338
+ }
339
+ inlineParts = inlineParts.replace(/\s+/g, ' ').trim();
340
+ return indent + marker + task + inlineParts + nested;
341
+ }
342
+
343
+ function rt_blockquote(node) {
344
+ var inner = rt_blockChildren(node);
345
+ var lines = inner.split('\n');
346
+ var out = [];
347
+ for (var i = 0; i < lines.length; i++) out.push(lines[i] ? '> ' + lines[i] : '>');
348
+ return out.join('\n');
349
+ }
350
+
351
+ function rt_blockChildren(node) {
352
+ var parts = [];
353
+ var kids = node.childNodes;
354
+ var i = 0;
355
+ while (i < kids.length) {
356
+ var k = kids[i];
357
+ // A selection can contain bare <li> elements (when the user selects a
358
+ // list's contents rather than the <ul>/<ol> wrapper). Group a run of them
359
+ // into a bullet list instead of emitting each as a separate block.
360
+ if (k.nodeType === 1 && k.tagName.toLowerCase() === 'li') {
361
+ var lis = [];
362
+ while (i < kids.length) {
363
+ var kk = kids[i];
364
+ if (kk.nodeType === 1 && kk.tagName.toLowerCase() === 'li') { lis.push(kk); i++; }
365
+ else if (kk.nodeType === 3 && !(kk.nodeValue || '').trim()) { i++; }
366
+ else break;
367
+ }
368
+ var items = [];
369
+ for (var j = 0; j < lis.length; j++) items.push(rt_listItem(lis[j], false, j + 1, 0));
370
+ parts.push(items.join('\n'));
371
+ continue;
372
+ }
373
+ var s = rt_blockNode(k);
374
+ if (s && s.replace(/\s/g, '') !== '') parts.push(s.replace(/\s+$/, ''));
375
+ i++;
376
+ }
377
+ return parts.join('\n\n');
378
+ }
379
+
380
+ function rt_blockNode(node) {
381
+ if (node.nodeType === 3) {
382
+ var t = node.nodeValue;
383
+ return (t && t.trim()) ? t.replace(/\s+/g, ' ').trim() : '';
384
+ }
385
+ if (node.nodeType !== 1) return '';
386
+ var dm = rt_attr(node, 'data-md');
387
+ if (dm != null) return dm;
388
+ if (node.classList) {
389
+ if (node.classList.contains('katex-display')) {
390
+ var k = node.querySelector ? node.querySelector('.katex') : null;
391
+ return rt_katex(k || node, true);
392
+ }
393
+ if (node.classList.contains('mermaid')) return '{{mermaid\n' + (node.textContent || '').trim() + '\n}}';
394
+ }
395
+ var tag = node.tagName.toLowerCase();
396
+ var hm = tag.match(/^h([1-6])$/);
397
+ if (hm) return rt_repeat('#', parseInt(hm[1], 10)) + ' ' + rt_inlineChildren(node).trim();
398
+ switch (tag) {
399
+ case 'p': return rt_inlineChildren(node).trim();
400
+ case 'ul': return rt_list(node, false, 0);
401
+ case 'ol': return rt_list(node, true, 0);
402
+ case 'pre': return rt_codeBlock(node);
403
+ case 'blockquote': return rt_blockquote(node);
404
+ case 'table': return rt_table(node);
405
+ case 'hr': return '---';
406
+ case 'li': return rt_inlineChildren(node).trim();
407
+ case 'figure': case 'div': case 'section': case 'article': case 'details': case 'summary':
408
+ return rt_blockChildren(node);
409
+ default: return rt_inlineNode(node).trim();
410
+ }
411
+ }
412
+
413
+ function elementToMarkdown(root) {
414
+ if (!root) return '';
415
+ var hasBlock = false;
416
+ var kids = root.childNodes || [];
417
+ for (var i = 0; i < kids.length; i++) {
418
+ if (rt_isBlockElement(kids[i])) { hasBlock = true; break; }
419
+ }
420
+ var out = hasBlock ? rt_blockChildren(root) : rt_inlineChildren(root);
421
+ return out.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').replace(/^\s+|\s+$/g, '');
422
+ }
423
+
424
+ function rt_isEditable(node) {
425
+ while (node) {
426
+ if (node.nodeType === 1) {
427
+ var tag = node.tagName.toLowerCase();
428
+ if (tag === 'input' || tag === 'textarea') return true;
429
+ if (node.isContentEditable) return true;
430
+ }
431
+ node = node.parentNode;
432
+ }
433
+ return false;
434
+ }
435
+
436
+ function rt_withinCopyRoot(node) {
437
+ while (node) {
438
+ if (node.nodeType === 1 && node.classList &&
439
+ (node.classList.contains('markdown-body') || rt_attr(node, 'data-orz-copy') != null)) return true;
440
+ node = node.parentNode;
441
+ }
442
+ return false;
443
+ }
444
+
445
+ function onCopy(event) {
446
+ if (!global.document) return;
447
+ var sel = global.getSelection ? global.getSelection()
448
+ : (global.document.getSelection ? global.document.getSelection() : null);
449
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
450
+ if (rt_isEditable(sel.anchorNode) || rt_isEditable(sel.focusNode)) return;
451
+ if (!rt_withinCopyRoot(sel.anchorNode) && !rt_withinCopyRoot(sel.focusNode)) return;
452
+ var container = global.document.createElement('div');
453
+ for (var i = 0; i < sel.rangeCount; i++) container.appendChild(sel.getRangeAt(i).cloneContents());
454
+ var md = elementToMarkdown(container);
455
+ if (!md) return;
456
+ var cd = event.clipboardData || global.clipboardData;
457
+ if (cd && cd.setData) {
458
+ cd.setData('text/plain', md);
459
+ if (event.preventDefault) event.preventDefault();
460
+ }
461
+ }
462
+
157
463
  function bindGlobalHandlers() {
158
- global.document.addEventListener('click', function () { collapseQr(); });
464
+ if (!global.document || global.__orzMarkdownRuntimeBound) return;
465
+ global.__orzMarkdownRuntimeBound = true;
466
+
467
+ global.document.addEventListener('click', function (event) {
468
+ if (expandedQr && !expandedQr.contains(event.target)) {
469
+ collapseQr();
470
+ }
471
+ });
472
+
159
473
  global.document.addEventListener('keydown', function (event) {
160
- if (event.key === 'Escape') collapseQr();
474
+ if (event.key === 'Escape') {
475
+ collapseQr();
476
+ }
161
477
  });
162
- }
163
478
 
164
- function init(doc) {
165
- initQrCodes(doc);
166
- bindGlobalHandlers();
479
+ // Copy rendered selections as Markdown source.
480
+ global.document.addEventListener('copy', onCopy);
167
481
  }
168
482
 
169
- global.OrzMarkdownRuntime = { init: init, initQrCodes: initQrCodes };
483
+ global.OrzMarkdownRuntime = Object.assign({}, global.OrzMarkdownRuntime, {
484
+ init: init,
485
+ initQrCodes: initQrCodes,
486
+ collapseQr: collapseQr,
487
+ elementToMarkdown: elementToMarkdown,
488
+ });
489
+
490
+ bindGlobalHandlers();
170
491
 
171
492
  if (global.document) {
172
493
  if (global.document.readyState === 'loading') {
173
- global.document.addEventListener('DOMContentLoaded', function () { init(global.document); }, { once: true });
494
+ global.document.addEventListener('DOMContentLoaded', function () {
495
+ init(global.document);
496
+ }, { once: true });
174
497
  } else {
175
498
  init(global.document);
176
499
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orz-markdown",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Customized markdown-it parser with official and custom plugins",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -64,6 +64,7 @@
64
64
  "@types/markdown-it-footnote": "^3.0.4",
65
65
  "@types/node": "^25.3.5",
66
66
  "@types/qrcode-svg": "^1.1.5",
67
+ "happy-dom": "^15.11.0",
67
68
  "tsx": "^4.21.0",
68
69
  "typescript": "^5.9.3",
69
70
  "vitest": "^4.0.18"