@helping-ai-workflow/md2doc 3.12.1 → 3.13.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/bin/md2doc.js +6 -0
- package/lib/editor/client.js +1015 -194
- package/lib/editor/inline-md.js +45 -1
- package/lib/editor/list-md.js +8 -1
- package/lib/editor/server.js +5 -0
- package/lib/editor/wave-canvas.js +301 -0
- package/lib/editor/wave-codec.js +417 -11
- package/lib/editor/wave-draw.js +209 -1202
- package/lib/editor/wave-geometry.js +145 -10
- package/lib/editor/wave-panels.js +907 -918
- package/lib/editor/wave-store.js +486 -91
- package/lib/editor/wave-ui.js +3588 -2232
- package/lib/md2doc.js +765 -534
- package/lib/theme/runtime.js +296 -29
- package/lib/theme/tokens.js +3 -0
- package/lib/wave-overflow.js +107 -0
- package/package.json +3 -3
- package/scripts/release-check.js +60 -0
package/lib/md2doc.js
CHANGED
|
@@ -23,6 +23,8 @@ const path = require('path');
|
|
|
23
23
|
const drawio = require('./drawio.js');
|
|
24
24
|
const { applyReaderTheme } = require('./theme/tokens');
|
|
25
25
|
const { highlightCode } = require('./highlight');
|
|
26
|
+
const waveCodec = require('./editor/wave-codec.js');
|
|
27
|
+
const waveOverflow = require('./wave-overflow.js');
|
|
26
28
|
|
|
27
29
|
// How long to let client-side WaveDrom / Mermaid scripts render before we
|
|
28
30
|
// snapshot the DOM (PDF print, or --bake-svg inert-SVG bake).
|
|
@@ -183,6 +185,9 @@ function buildKatexStyleTag() {
|
|
|
183
185
|
// it per-call would grow the registry unboundedly in a long-lived process
|
|
184
186
|
// (the editor server calls renderMarkdown() once per edit).
|
|
185
187
|
let marked, Renderer;
|
|
188
|
+
// v3.13.1 (T9b): set by renderMarkdown() around its synchronous parse — see the
|
|
189
|
+
// blockKatex renderer below. Null at every other time.
|
|
190
|
+
let blockMathText = null;
|
|
186
191
|
try {
|
|
187
192
|
({ marked, Renderer } = require('marked'));
|
|
188
193
|
|
|
@@ -230,6 +235,14 @@ try {
|
|
|
230
235
|
// default no-space-adjacency rules keep prose currency ($5 to $10) unrendered.
|
|
231
236
|
const markedKatex = require('marked-katex-extension');
|
|
232
237
|
marked.use(markedKatex({ throwOnError: false }));
|
|
238
|
+
// v3.13.1 (T9b): a $$ block renders through the extension's own renderer,
|
|
239
|
+
// which the per-call renderer overrides never reach, so the search index
|
|
240
|
+
// would miss it. This one hands its TeX to the render in progress, then
|
|
241
|
+
// returns false so the extension's renderer still draws the formula.
|
|
242
|
+
marked.use({ extensions: [{ name: 'blockKatex', renderer(token) {
|
|
243
|
+
if (blockMathText) blockMathText(token.text);
|
|
244
|
+
return false;
|
|
245
|
+
} }] });
|
|
233
246
|
} catch (e) {
|
|
234
247
|
console.error('[ERROR] marked not found — install with: npm install marked');
|
|
235
248
|
console.error(e.message);
|
|
@@ -333,7 +346,10 @@ function renderEditModeList(listToken, blocks, biRef, out) {
|
|
|
333
346
|
let inner;
|
|
334
347
|
if (!item.loose && ownTokens.length === 1 && ownTokens[0].type === 'text') {
|
|
335
348
|
// Tight item with a single text token: use static parseInline to get
|
|
336
|
-
// clean inline HTML without a <p> wrapper.
|
|
349
|
+
// clean inline HTML without a <p> wrapper. parseInline never reaches the
|
|
350
|
+
// block renderers, so the item's text is indexed for search here
|
|
351
|
+
// (v3.13.1 T9b; biRef.indexInline is set by renderMarkdown).
|
|
352
|
+
if (biRef.indexInline) biRef.indexInline(ownTokens[0].tokens);
|
|
337
353
|
inner = marked.Parser.parseInline(ownTokens[0].tokens);
|
|
338
354
|
} else {
|
|
339
355
|
// Loose item or non-standard own-token shape: use marked.parser, which
|
|
@@ -343,7 +359,10 @@ function renderEditModeList(listToken, blocks, biRef, out) {
|
|
|
343
359
|
// marked pretty-prints a trailing "\n" after the closing tag, which is
|
|
344
360
|
// insignificant to every serializer (list-md.js drops it via isBlankText)
|
|
345
361
|
// but would render as a phantom blank line inside the item.
|
|
346
|
-
|
|
362
|
+
// biRef.parseItem (renderMarkdown) is marked.parser with the search
|
|
363
|
+
// index told that a text run here is wrapped in a made-up paragraph,
|
|
364
|
+
// as in a loose item, so each item is indexed once (v3.13.1 T9b).
|
|
365
|
+
inner = (biRef.parseItem ? biRef.parseItem(ownTokens) : marked.parser(ownTokens)).trim();
|
|
347
366
|
}
|
|
348
367
|
// The receiving half of list-md.js's SETEXT ESCAPE (see
|
|
349
368
|
// escapeSetextHazard() there): an empty list item that is the first child
|
|
@@ -730,21 +749,27 @@ async function renderMarkdown(mdText, srcPath, opts = {}) {
|
|
|
730
749
|
const sections = [];
|
|
731
750
|
let currentSection = null;
|
|
732
751
|
|
|
733
|
-
|
|
752
|
+
// The search index: one string per section, built from the text the
|
|
753
|
+
// reader sees, the way the in-text marks read it (highlightFirstOccurrence
|
|
754
|
+
// in the reader runtime). Both sides must agree, or the results list finds
|
|
755
|
+
// a phrase the reader cannot mark.
|
|
756
|
+
function startSection({ id, depth, text, search }) {
|
|
734
757
|
currentSection = {
|
|
735
758
|
id,
|
|
736
759
|
depth,
|
|
737
760
|
title: text,
|
|
738
|
-
searchTextParts: [text],
|
|
761
|
+
searchTextParts: [search === undefined ? text : search.replace(/\s+/g, ' ').trim()],
|
|
739
762
|
};
|
|
740
763
|
sections.push(currentSection);
|
|
741
764
|
}
|
|
742
765
|
|
|
766
|
+
// Takes plain text (sectionInlineText, code, TeX): no tag stripping here,
|
|
767
|
+
// so code such as "if (a<b && c>d)" is indexed as written.
|
|
743
768
|
function appendSectionText(value) {
|
|
744
769
|
if (!currentSection || !value) {
|
|
745
770
|
return;
|
|
746
771
|
}
|
|
747
|
-
const clean =
|
|
772
|
+
const clean = String(value).replace(/\s+/g, ' ').trim();
|
|
748
773
|
if (clean) {
|
|
749
774
|
currentSection.searchTextParts.push(clean);
|
|
750
775
|
}
|
|
@@ -756,13 +781,37 @@ async function renderMarkdown(mdText, srcPath, opts = {}) {
|
|
|
756
781
|
}
|
|
757
782
|
for (const cell of cells) {
|
|
758
783
|
if (cell && Array.isArray(cell.tokens)) {
|
|
759
|
-
appendSectionText(
|
|
784
|
+
appendSectionText(sectionInlineText(cell.tokens));
|
|
760
785
|
} else if (cell && typeof cell.text === 'string') {
|
|
761
|
-
appendSectionText(cell.text);
|
|
786
|
+
appendSectionText(stripHtmlTags(cell.text));
|
|
762
787
|
}
|
|
763
788
|
}
|
|
764
789
|
}
|
|
765
790
|
|
|
791
|
+
// v3.13.1 (T9b): inline tokens as the reader sees them, for the search
|
|
792
|
+
// index. Inline math is its TeX source (the reader runtime reads a formula
|
|
793
|
+
// from its TeX annotation); a hard break or a literal <br> is a space (the
|
|
794
|
+
// runtime reads a br element as one); other inline html carries no text;
|
|
795
|
+
// an image's alt text is not on the page. A text token's text is escaped
|
|
796
|
+
// by marked, so a tag in it is real markup (the checkbox a loose task item
|
|
797
|
+
// gets) and is stripped before the entities are decoded.
|
|
798
|
+
function sectionInlineText(tokens) {
|
|
799
|
+
if (!Array.isArray(tokens)) {
|
|
800
|
+
return '';
|
|
801
|
+
}
|
|
802
|
+
return tokens
|
|
803
|
+
.map((item) => {
|
|
804
|
+
if (item.type === 'br') return ' ';
|
|
805
|
+
if (item.type === 'html') return /^<br\b/i.test(item.text || '') ? ' ' : '';
|
|
806
|
+
if (item.type === 'image') return '';
|
|
807
|
+
if (item.type === 'inlineKatex') return item.text || '';
|
|
808
|
+
if (item.type === 'codespan') return unescapeHtml(item.text || '');
|
|
809
|
+
if (item.tokens) return sectionInlineText(item.tokens);
|
|
810
|
+
return unescapeHtml(stripHtmlTags(String(item.text || '').replace(/<br\b[^>]*>/gi, ' ')));
|
|
811
|
+
})
|
|
812
|
+
.join('');
|
|
813
|
+
}
|
|
814
|
+
|
|
766
815
|
function buildTocTree(items) {
|
|
767
816
|
const root = [];
|
|
768
817
|
const stack = [{ depth: 0, children: root }];
|
|
@@ -976,9 +1025,64 @@ ${itemsHtml}
|
|
|
976
1025
|
return out + '>';
|
|
977
1026
|
};
|
|
978
1027
|
|
|
1028
|
+
// R7 甲 (v3.13.1): a block that is nothing but one or more native <svg>
|
|
1029
|
+
// elements gets the class inline-svg, so dark mode can put it on a white
|
|
1030
|
+
// plate like images and draw.io. Decided on the block's own source, not
|
|
1031
|
+
// its tokens: marked lexes '<svg><text>x</text></svg>' on one line as a
|
|
1032
|
+
// paragraph of html,html,text,html,html tokens, and '<svg ...>' alone on
|
|
1033
|
+
// its first line as one block html token. Outside the svg elements only
|
|
1034
|
+
// whitespace may appear, so an svg inside prose is never wrapped. Reader
|
|
1035
|
+
// output only: edit mode keeps its block DOM exactly as it was.
|
|
1036
|
+
// Not covered: an svg in a tight list item or a table cell reaches the
|
|
1037
|
+
// renderer as inline tokens with no block of its own, so it gets no plate.
|
|
1038
|
+
// HTML comments are dropped before any <svg> is looked at: '<!-- <svg> -->'
|
|
1039
|
+
// opens nothing.
|
|
1040
|
+
function noComments(src) { return String(src || '').replace(/<!--[\s\S]*?-->/g, ''); }
|
|
1041
|
+
function isSvgOnly(src) {
|
|
1042
|
+
const s = noComments(src).trim();
|
|
1043
|
+
if (!/^<svg[\s>]/i.test(s) || !/<\/svg>$/i.test(s)) return false;
|
|
1044
|
+
const tag = /<(\/?)svg\b[^>]*?(\/?)>/gi;
|
|
1045
|
+
let depth = 0, last = 0, m;
|
|
1046
|
+
while ((m = tag.exec(s)) !== null) {
|
|
1047
|
+
if (depth === 0 && s.slice(last, m.index).trim()) return false;
|
|
1048
|
+
if (m[1]) depth--; else if (!m[2]) depth++;
|
|
1049
|
+
if (depth < 0) return false;
|
|
1050
|
+
last = tag.lastIndex;
|
|
1051
|
+
}
|
|
1052
|
+
return depth === 0 && !s.slice(last).trim();
|
|
1053
|
+
}
|
|
1054
|
+
// An outer <svg> that a blank line split into several html blocks is still
|
|
1055
|
+
// open while its later blocks render; a <div> there would close it early, so
|
|
1056
|
+
// no html block is wrapped until it closes. Only raw html blocks can keep an
|
|
1057
|
+
// svg open: a paragraph's <p> and </p> end any open svg in the browser's
|
|
1058
|
+
// parser (measured in Chromium and WebKit), so a paragraph always resets the
|
|
1059
|
+
// count, and text in a code span or an svg left unclosed inside a paragraph
|
|
1060
|
+
// never holds later blocks hostage. Counted per block, in document order.
|
|
1061
|
+
let openSvg = 0;
|
|
1062
|
+
function svgNet(src) {
|
|
1063
|
+
const tag = /<(\/?)svg\b[^>]*?(\/?)>/gi;
|
|
1064
|
+
let n = 0, m;
|
|
1065
|
+
const s = noComments(src);
|
|
1066
|
+
while ((m = tag.exec(s)) !== null) { if (m[1]) n--; else if (!m[2]) n++; }
|
|
1067
|
+
return n;
|
|
1068
|
+
}
|
|
1069
|
+
function wrapsSvg(src) {
|
|
1070
|
+
const wrap = openSvg === 0 && isSvgOnly(src);
|
|
1071
|
+
openSvg = Math.max(0, openSvg + svgNet(src));
|
|
1072
|
+
return wrap;
|
|
1073
|
+
}
|
|
1074
|
+
function wrapsSvgParagraph(src) {
|
|
1075
|
+
openSvg = 0;
|
|
1076
|
+
return isSvgOnly(src);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
979
1079
|
const baseHtml = renderer.html.bind(renderer);
|
|
980
1080
|
renderer.html = function(token) {
|
|
981
|
-
|
|
1081
|
+
const out = inlineImagesInHtmlChunk(baseHtml.apply(this, arguments));
|
|
1082
|
+
if (!opts.editMode && token && token.block && wrapsSvg(token.text)) {
|
|
1083
|
+
return '<div class="inline-svg">' + out.trim() + '</div>\n';
|
|
1084
|
+
}
|
|
1085
|
+
return out;
|
|
982
1086
|
};
|
|
983
1087
|
|
|
984
1088
|
// Spec §3.12 — a markdown HARD BREAK and a literal '<br>' the source spelled
|
|
@@ -1004,6 +1108,7 @@ ${itemsHtml}
|
|
|
1004
1108
|
renderer.br = function() { return '<br data-hard-break="1">'; };
|
|
1005
1109
|
}
|
|
1006
1110
|
|
|
1111
|
+
let waveBlockCount = 0;
|
|
1007
1112
|
renderer.code = function(token) {
|
|
1008
1113
|
// token is either a string (old API) or {text, lang} object (new API)
|
|
1009
1114
|
const lang = (typeof token === 'object') ? (token.lang || '') : (arguments[1] || '');
|
|
@@ -1011,6 +1116,29 @@ ${itemsHtml}
|
|
|
1011
1116
|
|
|
1012
1117
|
if (lang === 'wavedrom') {
|
|
1013
1118
|
usesWaveDrom = true;
|
|
1119
|
+
waveBlockCount++;
|
|
1120
|
+
// CLI-only: the CLI passes its source path as opts.warnWaveOverflow;
|
|
1121
|
+
// library and --edit renders leave it unset and stay silent.
|
|
1122
|
+
// One line per diagram, never one per label (final review M1): the
|
|
1123
|
+
// per-label lines of one diagram each named their own scale, and
|
|
1124
|
+
// following the first ("set to 2") left the next one ("to 4") still
|
|
1125
|
+
// overflowing. The line names the count, the label that needs the
|
|
1126
|
+
// most, and the one scale that fits them all (suggestScale).
|
|
1127
|
+
if (opts.warnWaveOverflow) {
|
|
1128
|
+
try {
|
|
1129
|
+
const parsed = waveCodec.parseSource(code);
|
|
1130
|
+
const list = parsed.ok ? waveOverflow.overflowOf(parsed.doc) : [];
|
|
1131
|
+
if (list.length > 0) {
|
|
1132
|
+
let worst = list[0];
|
|
1133
|
+
for (const o of list) if (o.needScale > worst.needScale) worst = o;
|
|
1134
|
+
const named = '"' + worst.label + '" in ' + worst.name;
|
|
1135
|
+
const what = list.length === 1
|
|
1136
|
+
? '1 data label is wider than its segment (' + named + ')'
|
|
1137
|
+
: list.length + ' data labels are wider than their segments (worst: ' + named + ')';
|
|
1138
|
+
console.error(`md2doc: ${opts.warnWaveOverflow}: wavedrom #${waveBlockCount}: ${what}; set config.hscale to ${waveOverflow.suggestScale(list)} (estimated)`);
|
|
1139
|
+
}
|
|
1140
|
+
} catch (e) { /* a warning must never break a render */ }
|
|
1141
|
+
}
|
|
1014
1142
|
return `\n<script type="WaveDrom">\n${code}\n</script>\n`;
|
|
1015
1143
|
}
|
|
1016
1144
|
if (lang === 'mermaid') {
|
|
@@ -1022,6 +1150,8 @@ ${itemsHtml}
|
|
|
1022
1150
|
return `\n<div class="mermaid">\n${escapeHtml(code)}\n</div>\n`;
|
|
1023
1151
|
}
|
|
1024
1152
|
if (lang === 'math') {
|
|
1153
|
+
// Indexed as its TeX, like inline and $$ math (T9b).
|
|
1154
|
+
appendSectionText(code);
|
|
1025
1155
|
// KaTeX renders synchronously to static HTML (class="katex"); no client
|
|
1026
1156
|
// runtime, no async post-pass. throwOnError:false degrades a bad formula
|
|
1027
1157
|
// to red error text instead of crashing the whole render.
|
|
@@ -1042,7 +1172,9 @@ ${itemsHtml}
|
|
|
1042
1172
|
}
|
|
1043
1173
|
// Default: fenced code. v3.10.0 colours registered languages here, at
|
|
1044
1174
|
// render time (lib/highlight.js); an unknown or missing language stays
|
|
1045
|
-
// plain escaped text.
|
|
1175
|
+
// plain escaped text. v3.13.1 (T9b): indexed for search as written, so a
|
|
1176
|
+
// name that only a code block holds is found and marked.
|
|
1177
|
+
appendSectionText(code);
|
|
1046
1178
|
const coloured = highlightCode(code, lang);
|
|
1047
1179
|
// The info string goes into an attribute: escape it (an info string such
|
|
1048
1180
|
// as x" onmouseover=… must stay text). Ordinary language names are unchanged.
|
|
@@ -1077,21 +1209,54 @@ ${itemsHtml}
|
|
|
1077
1209
|
id: headingId,
|
|
1078
1210
|
text: headingText,
|
|
1079
1211
|
});
|
|
1080
|
-
startSection({ depth, id: headingId, text: headingText });
|
|
1212
|
+
startSection({ depth, id: headingId, text: headingText, search: sectionInlineText(token.tokens) });
|
|
1081
1213
|
|
|
1082
1214
|
return `<h${depth} id="${headingId}" class="heading-with-anchor" data-reader-heading data-reader-depth="${depth}">${headingHtml}<a class="heading-anchor" href="#${headingId}" aria-label="Link to this section">#</a></h${depth}>\n`;
|
|
1083
1215
|
};
|
|
1084
1216
|
|
|
1217
|
+
// A list item's text is indexed as it renders, in reading order with its
|
|
1218
|
+
// block children: marked's parser hands each of the item's block-level
|
|
1219
|
+
// text tokens (the only text tokens that carry inline .tokens) to
|
|
1220
|
+
// renderer.text. In a loose item the parser then wraps that run of text in
|
|
1221
|
+
// a paragraph it makes up from the HTML it just rendered (a formula as
|
|
1222
|
+
// KaTeX markup); renderer.text arms a one-shot skip for that paragraph.
|
|
1223
|
+
// Real paragraph tokens (in a loose item, those after a nested list) are
|
|
1224
|
+
// indexed by renderer.paragraph as usual.
|
|
1225
|
+
let looseItemText = false;
|
|
1226
|
+
let skipMadeUpParagraph = false;
|
|
1227
|
+
const baseText = renderer.text.bind(renderer);
|
|
1228
|
+
renderer.text = function(token) {
|
|
1229
|
+
if (token && Array.isArray(token.tokens)) {
|
|
1230
|
+
appendSectionText(sectionInlineText(token.tokens));
|
|
1231
|
+
if (looseItemText) skipMadeUpParagraph = true;
|
|
1232
|
+
}
|
|
1233
|
+
return baseText(token);
|
|
1234
|
+
};
|
|
1085
1235
|
const baseParagraph = renderer.paragraph.bind(renderer);
|
|
1086
1236
|
renderer.paragraph = function(token) {
|
|
1087
|
-
|
|
1237
|
+
if (skipMadeUpParagraph) {
|
|
1238
|
+
skipMadeUpParagraph = false;
|
|
1239
|
+
} else {
|
|
1240
|
+
appendSectionText(sectionInlineText(token.tokens));
|
|
1241
|
+
}
|
|
1242
|
+
// R7 甲: see isSvgOnly above renderer.html.
|
|
1243
|
+
if (!opts.editMode && wrapsSvgParagraph(token.raw)) {
|
|
1244
|
+
return baseParagraph(token).replace(/^<p>/, '<p class="inline-svg">');
|
|
1245
|
+
}
|
|
1088
1246
|
return baseParagraph(token);
|
|
1089
1247
|
};
|
|
1090
1248
|
|
|
1091
1249
|
const baseListitem = renderer.listitem.bind(renderer);
|
|
1092
1250
|
renderer.listitem = function(token) {
|
|
1093
|
-
|
|
1094
|
-
const
|
|
1251
|
+
// Indexes nothing itself: see renderer.text above.
|
|
1252
|
+
const outerLoose = looseItemText;
|
|
1253
|
+
looseItemText = !!token.loose;
|
|
1254
|
+
let html;
|
|
1255
|
+
try {
|
|
1256
|
+
html = baseListitem(token);
|
|
1257
|
+
} finally {
|
|
1258
|
+
looseItemText = outerLoose;
|
|
1259
|
+
}
|
|
1095
1260
|
if (!token.task) return html;
|
|
1096
1261
|
// v3.10.0: a task item drops its bullet and keeps its own text in a span,
|
|
1097
1262
|
// so a checked item's strike-through stops at its own words — CSS
|
|
@@ -1107,8 +1272,14 @@ ${itemsHtml}
|
|
|
1107
1272
|
|
|
1108
1273
|
const baseBlockquote = renderer.blockquote.bind(renderer);
|
|
1109
1274
|
renderer.blockquote = function(token) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1275
|
+
// Its paragraphs index themselves (indexing them here too doubled them).
|
|
1276
|
+
const outerLoose = looseItemText;
|
|
1277
|
+
looseItemText = false;
|
|
1278
|
+
try {
|
|
1279
|
+
return baseBlockquote(token);
|
|
1280
|
+
} finally {
|
|
1281
|
+
looseItemText = outerLoose;
|
|
1282
|
+
}
|
|
1112
1283
|
};
|
|
1113
1284
|
|
|
1114
1285
|
renderer.table = function(token) {
|
|
@@ -1179,6 +1350,8 @@ ${itemsHtml}
|
|
|
1179
1350
|
return `<a href="#${escAttr(slug)}">[${escAttr(body)}]</a>`;
|
|
1180
1351
|
});
|
|
1181
1352
|
|
|
1353
|
+
// A $$ block's TeX reaches the index through the blockKatex hook at the
|
|
1354
|
+
// top of the file, set only while this render's synchronous parse runs.
|
|
1182
1355
|
if (opts.editMode) {
|
|
1183
1356
|
// Ranges are computed on mdPre. The [[...]] preprocessing above only
|
|
1184
1357
|
// replaces text within single lines and never adds/removes newlines,
|
|
@@ -1198,19 +1371,37 @@ ${itemsHtml}
|
|
|
1198
1371
|
// flat list renderer above threads through its own recursion (see
|
|
1199
1372
|
// liRunStartsHere()).
|
|
1200
1373
|
const biRef = { v: 0, prevLi: null, types: [] };
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1374
|
+
// The edit page searches this index too: renderEditModeList indexes each
|
|
1375
|
+
// item through these two, once, in the edit DOM's order (an item's own
|
|
1376
|
+
// block, then its nested lists).
|
|
1377
|
+
biRef.indexInline = (inlineTokens) => appendSectionText(sectionInlineText(inlineTokens));
|
|
1378
|
+
biRef.parseItem = (ownTokens) => {
|
|
1379
|
+
const outerLoose = looseItemText;
|
|
1380
|
+
looseItemText = true;
|
|
1381
|
+
try {
|
|
1382
|
+
return marked.parser(ownTokens);
|
|
1383
|
+
} finally {
|
|
1384
|
+
looseItemText = outerLoose;
|
|
1206
1385
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1386
|
+
};
|
|
1387
|
+
try {
|
|
1388
|
+
blockMathText = appendSectionText;
|
|
1389
|
+
for (const t of tokens) {
|
|
1390
|
+
if (t.type === 'space') continue;
|
|
1391
|
+
if (t.type === 'list') {
|
|
1392
|
+
parts.push(...renderEditModeList(t, blocks, biRef));
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
resetLiRunState(biRef); // §3.8 rule (c): a non-li block ends every run
|
|
1396
|
+
const b = blocks[biRef.v++];
|
|
1397
|
+
const inner = marked.parser([t]);
|
|
1398
|
+
parts.push(
|
|
1399
|
+
`<div class="ed-block" data-block-id="${b.id}" data-block-type="${b.type}">` +
|
|
1400
|
+
inner + '</div>'
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
} finally {
|
|
1404
|
+
blockMathText = null;
|
|
1214
1405
|
}
|
|
1215
1406
|
// Development-time alignment guard: the render walk and blockmap.js's
|
|
1216
1407
|
// pushListItemBlocks must consume blocks[] in lockstep. A mismatch means
|
|
@@ -1244,7 +1435,12 @@ ${itemsHtml}
|
|
|
1244
1435
|
}
|
|
1245
1436
|
bodyHtml = parts.join('\n');
|
|
1246
1437
|
} else {
|
|
1247
|
-
|
|
1438
|
+
try {
|
|
1439
|
+
blockMathText = appendSectionText;
|
|
1440
|
+
bodyHtml = marked.parse(mdPre);
|
|
1441
|
+
} finally {
|
|
1442
|
+
blockMathText = null;
|
|
1443
|
+
}
|
|
1248
1444
|
}
|
|
1249
1445
|
// Both the ```math fence and the $/$$ extension emit class="katex" — a single
|
|
1250
1446
|
// post-parse scan is the source of truth for conditional CSS injection.
|
|
@@ -1260,6 +1456,11 @@ ${itemsHtml}
|
|
|
1260
1456
|
).replace(/</g, '\\u003c');
|
|
1261
1457
|
if (tocItems.length > 0) {
|
|
1262
1458
|
const tocTree = buildTocTree(tocItems);
|
|
1459
|
+
// v3.13.1 (R3): two or more H1s kept as the TOC's top level are
|
|
1460
|
+
// chapters; the marker lets CSS set each chapter apart with a rule and
|
|
1461
|
+
// a gap. A flattened single-H1 TOC (its H2s on top) never carries it.
|
|
1462
|
+
const tocRoots = flattenSingleRoot(tocTree);
|
|
1463
|
+
const tocChapters = tocRoots === tocTree && tocTree.filter((n) => n.depth === 1).length >= 2;
|
|
1263
1464
|
tocHtml = `<aside class="reader-sidebar" data-reader-sidebar>
|
|
1264
1465
|
<section class="reader-tools">
|
|
1265
1466
|
<label class="reader-search-label" for="doc-search-input">Search</label>
|
|
@@ -1278,7 +1479,7 @@ ${itemsHtml}
|
|
|
1278
1479
|
</div>
|
|
1279
1480
|
<div id="search-results-list"></div>
|
|
1280
1481
|
</section>
|
|
1281
|
-
<nav class="toc" aria-label="Table of contents" data-reader-toc>
|
|
1482
|
+
<nav class="toc" aria-label="Table of contents" data-reader-toc${tocChapters ? ' data-toc-chapters' : ''}>
|
|
1282
1483
|
<div class="toc-header">
|
|
1283
1484
|
<span class="toc-title">Contents</span>
|
|
1284
1485
|
<div class="toc-header-actions">
|
|
@@ -1288,7 +1489,7 @@ ${itemsHtml}
|
|
|
1288
1489
|
</div>
|
|
1289
1490
|
<div class="toc-breadcrumb" data-toc-breadcrumb aria-label="Current location"></div>
|
|
1290
1491
|
</div>
|
|
1291
|
-
${renderTocNodes(
|
|
1492
|
+
${renderTocNodes(tocRoots)}
|
|
1292
1493
|
</nav>
|
|
1293
1494
|
</aside>
|
|
1294
1495
|
<div class="sidebar-splitter" id="sidebar-splitter" role="separator" aria-orientation="vertical" aria-label="Resize sidebar"></div>`;
|
|
@@ -1787,488 +1988,341 @@ ${edUlMarkerCss}${edOlCounterCss}
|
|
|
1787
1988
|
position: fixed; inset: 0; z-index: 998; background: rgba(0,0,0,.45);
|
|
1788
1989
|
display: flex; align-items: center; justify-content: center;
|
|
1789
1990
|
}
|
|
1790
|
-
/*
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
user nothing"). 1920px is chosen so 94vw is the actual governor at
|
|
1797
|
-
every one of those four measured sizes (94% of 1920 is 1804.8, still
|
|
1798
|
-
under the new cap) — this number is a ceiling for a screen wider than
|
|
1799
|
-
that, not a value that binds within the measured range. At 800x600,
|
|
1800
|
-
94vw (752px) was already narrower than either cap, so that viewport is
|
|
1801
|
-
unchanged by this. See task-13-report.md for the re-measured
|
|
1802
|
-
toolbar/canvas table. */
|
|
1991
|
+
/* Wave redesign Task 6b (spec section 4.1): one column, top to bottom:
|
|
1992
|
+
title bar, toolbar, the notice card, the stage, the hint bar. The side
|
|
1993
|
+
rail is gone and the stage takes the whole width. The width cap of
|
|
1994
|
+
1920px is v3.6.0 Task 13's: below it, 94vw governs at every measured
|
|
1995
|
+
screen size. position: relative because the toolbar's popovers are
|
|
1996
|
+
placed against the panel. */
|
|
1803
1997
|
.ed-wave-panel {
|
|
1804
|
-
background: #fff; border-radius: 6px; width: min(1920px, 94vw);
|
|
1998
|
+
position: relative; background: #fff; border-radius: 6px; width: min(1920px, 94vw);
|
|
1805
1999
|
height: min(760px, 90vh); display: flex; flex-direction: column;
|
|
1806
2000
|
box-shadow: 0 6px 28px rgba(0,0,0,.35); overflow: hidden;
|
|
1807
2001
|
}
|
|
1808
2002
|
.ed-wave-head {
|
|
1809
|
-
display: flex; align-items: center;
|
|
1810
|
-
padding: 8px 12px; border-bottom: 1px solid #e2e2e2;
|
|
1811
|
-
}
|
|
2003
|
+
display: flex; align-items: center; gap: 12px;
|
|
2004
|
+
padding: 8px 12px; border-bottom: 1px solid #e2e2e2;
|
|
2005
|
+
}
|
|
2006
|
+
.ed-wave-title { font-weight: bold; }
|
|
2007
|
+
/* The FILE's save state (spec section 4.8), the same two states and
|
|
2008
|
+
colours as the edit toolbar's own status slot. */
|
|
2009
|
+
.ed-wave-save { margin-left: auto; font-size: 12px; color: var(--md-ed-ok); }
|
|
2010
|
+
.ed-wave-save[data-dirty="1"] { color: var(--md-ed-dirty); }
|
|
1812
2011
|
.ed-wave-close {
|
|
1813
|
-
border:
|
|
1814
|
-
|
|
1815
|
-
}
|
|
1816
|
-
/* Which way out keeps the work and which throws it away. Escape is a modal's
|
|
1817
|
-
universal close key and here it discards a whole session, so the header
|
|
1818
|
-
says so next to the button that does not. Normal weight against the bar's
|
|
1819
|
-
bold heading: it is a caption, not a warning the user has to clear. */
|
|
1820
|
-
.ed-wave-escape-hint {
|
|
1821
|
-
margin-left: auto; margin-right: 12px; font-weight: normal;
|
|
1822
|
-
font-size: 11px; color: #7a4b00;
|
|
2012
|
+
border: 0; border-radius: 4px; background: transparent; color: #555;
|
|
2013
|
+
width: 28px; height: 28px; font-size: 15px; line-height: 1; cursor: pointer;
|
|
1823
2014
|
}
|
|
2015
|
+
.ed-wave-close:hover { background: var(--md-ed-hover); }
|
|
2016
|
+
/* Spec section 4.2: one row; groups are divided by a thin rule and 完成
|
|
2017
|
+
sits on the right. Buttons are quiet until hovered or switched on. */
|
|
1824
2018
|
.ed-wave-toolbar {
|
|
1825
|
-
display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
|
|
1826
|
-
padding: 8px 12px; border-bottom: 1px solid #e2e2e2; background: #fafafa;
|
|
1827
|
-
}
|
|
1828
|
-
/* v3.6.0 Task 12: the eight captioned sections themselves. .ed-wave-tools
|
|
1829
|
-
is the row that holds them (the SAME element as .ed-wave-toolbar
|
|
1830
|
-
above -- that class still owns the bar's padding/border/background,
|
|
1831
|
-
this one owns only the group row's own gap). Each .ed-wave-tool-group
|
|
1832
|
-
is a bordered chip so eight adjacent sections read as eight things, not
|
|
1833
|
-
one longer wall of buttons; the caption sits to its buttons' LEFT (see
|
|
1834
|
-
wave-panels.js's toolGroup() for why not above). */
|
|
1835
|
-
.ed-wave-tools { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
|
1836
|
-
.ed-wave-tool-group {
|
|
1837
|
-
display: flex; align-items: center; gap: 4px;
|
|
1838
|
-
padding: 2px 6px; border: 1px solid #ececec; border-radius: 6px;
|
|
1839
|
-
}
|
|
1840
|
-
.ed-wave-tool-caption {
|
|
1841
|
-
font-size: 10px; color: #8a8a8a; letter-spacing: .5px;
|
|
1842
|
-
white-space: nowrap; margin-right: 2px;
|
|
1843
|
-
}
|
|
1844
|
-
.ed-wave-brushes, .ed-wave-meta {
|
|
1845
2019
|
display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
|
|
2020
|
+
padding: 6px 12px; border-bottom: 1px solid #e2e2e2; background: #fafafa;
|
|
1846
2021
|
}
|
|
1847
|
-
/* v3.6.0 Task 10: widened from a 28x28 bare-character square to a small
|
|
1848
|
-
column (glyph on top, the character kept as a caption underneath) so
|
|
1849
|
-
the 22-brush palette says what each brush paints instead of relying on
|
|
1850
|
-
the letter alone -- nine of those 22 (P, N, u, d, h, l, 2, 6-9) have no
|
|
1851
|
-
obvious on-canvas picture from the character by itself. */
|
|
1852
|
-
/* '.ed-wave-brushes .ed-wave-brush', not the bare class: '.ed-wave-toolbar
|
|
1853
|
-
button' below outranks a single class on specificity (class+type beats
|
|
1854
|
-
class alone) and would otherwise win the padding fight and squeeze the
|
|
1855
|
-
24px-wide icon into a content box narrower than it is. */
|
|
1856
|
-
.ed-wave-brushes .ed-wave-brush {
|
|
1857
|
-
width: 34px; height: 40px; border: 1px solid #c4c4c4; border-radius: 4px;
|
|
1858
|
-
background: #fff; font-family: monospace; cursor: pointer;
|
|
1859
|
-
display: flex; flex-direction: column; align-items: center;
|
|
1860
|
-
justify-content: center; gap: 1px; padding: 2px 0; box-sizing: border-box;
|
|
1861
|
-
}
|
|
1862
|
-
.ed-wave-brush.is-on { background: #1a73e8; color: #fff; border-color: #1a73e8; }
|
|
1863
|
-
.ed-wave-brush-char { font-size: 11px; line-height: 1; }
|
|
1864
|
-
/* The glyph itself. Its shapes are drawn by wave-draw.js's levelIcon,
|
|
1865
|
-
which reuses renderCanvas's own drawing functions (bandOf, brickPath,
|
|
1866
|
-
the bus/clock/gap helpers) -- never a second hand-drawn set, so a
|
|
1867
|
-
change to how a brush paints on the real canvas cannot leave the
|
|
1868
|
-
toolbar showing the old picture. */
|
|
1869
|
-
.ed-wave-level-icon { display: block; overflow: visible; }
|
|
1870
2022
|
.ed-wave-toolbar button {
|
|
1871
|
-
border: 1px solid
|
|
1872
|
-
padding:
|
|
1873
|
-
}
|
|
1874
|
-
.ed-wave-toolbar button:
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
}
|
|
1900
|
-
/* The
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
.ed-wave-nolanes { color: #7a1f00; background: #ffe9e0; border-color: #e8a987; }
|
|
1905
|
-
/* v3.6.0 Task 13 fix round 1 (Critical 2): position: relative added.
|
|
1906
|
-
.ed-wave-side[data-expanded] below is position: absolute, and neither
|
|
1907
|
-
.ed-wave-panel nor (until this line) this element had a position of
|
|
1908
|
-
their own — so its containing block was .ed-wave-overlay
|
|
1909
|
-
(position: fixed; inset: 0), the WHOLE viewport, not the dialog.
|
|
1910
|
-
.ed-wave-panel's own overflow: hidden did not clip it either: clipping
|
|
1911
|
-
follows the containing-block chain, not DOM nesting, so the rail
|
|
1912
|
-
rendered against viewport corners at a narrow window and covered
|
|
1913
|
-
.ed-wave-head's 關閉 button — the only discoverable way to close the
|
|
1914
|
-
dialog (a hit-test at 關閉's own rectangle returned the edge panel's
|
|
1915
|
-
delete button instead; Escape still worked but was not discoverable).
|
|
1916
|
-
Anchoring the rail to THIS element rather than to .ed-wave-panel itself
|
|
1917
|
-
is deliberate, not incidental: .ed-wave-body sits below .ed-wave-head
|
|
1918
|
-
and above .ed-wave-status, so top: 0 / bottom: 0 on the rail now lands
|
|
1919
|
-
at body's own top/bottom — never over the header (關閉) or the status
|
|
1920
|
-
line — without this rule having to know either one's height. */
|
|
1921
|
-
.ed-wave-body { display: flex; flex: 1 1 auto; min-height: 0; position: relative; }
|
|
1922
|
-
/* v3.6.0 Task 13: the old comment here hand-computed the canvas's leftover
|
|
1923
|
-
width from the (now gone) .ed-wave-preview column's own flex-basis;
|
|
1924
|
-
that arithmetic no longer applies — the third column is .ed-wave-side
|
|
1925
|
-
below, which collapses to a 28px rail under 1100px specifically so the
|
|
1926
|
-
canvas is not squeezed the way a fixed 160px minimum used to squeeze it
|
|
1927
|
-
at 800x600 (MEASURED before this task: canvas width there was only
|
|
1928
|
-
282px). Re-measured after this task at four real viewports; see
|
|
1929
|
-
task-13-report.md rather than a formula here — a number derived by hand
|
|
1930
|
-
drifts the moment any sibling column's own width changes, which is
|
|
1931
|
-
exactly what this task did. */
|
|
1932
|
-
.ed-wave-lanes {
|
|
1933
|
-
flex: 0 0 200px; border-right: 1px solid #e2e2e2; overflow: auto;
|
|
1934
|
-
padding: 8px 0 8px 0;
|
|
1935
|
-
}
|
|
1936
|
-
/* The rows are absolutely positioned at index * laneHeight so they line up
|
|
1937
|
-
with the drawing beside them cell for cell. A group title is NOT a row —
|
|
1938
|
-
measured against the pinned engine in Task 2 — so it is drawn as a label in
|
|
1939
|
-
the rail spanning its lanes' rows instead, which is the shape the engine
|
|
1940
|
-
itself gives it (horizontal space in the name column, no vertical space). */
|
|
1941
|
-
.ed-wave-lane-rows { position: relative; }
|
|
1942
|
-
/* v3.6.0 Task 14: a row is a drag handle, a name field and (on a {}
|
|
1943
|
-
spacer) a badge. It used to be six buttons plus the field, and the six
|
|
1944
|
-
did not fit: flexed into what was left of 200px the field measured
|
|
1945
|
-
45px on the most crowded row, and a browser given a 24px box and a
|
|
1946
|
-
three-character Chinese caption lays that caption out VERTICALLY — the
|
|
1947
|
-
editor shipped columns of stacked glyphs where labels were meant.
|
|
1948
|
-
Those six are toolbar buttons now (the 訊號 / 群組 sections), acting
|
|
1949
|
-
on the one selection. */
|
|
1950
|
-
.ed-wave-lane-row {
|
|
1951
|
-
position: absolute; left: 0; right: 0; display: flex; align-items: center;
|
|
1952
|
-
gap: 6px; padding-right: 6px; box-sizing: border-box;
|
|
1953
|
-
}
|
|
1954
|
-
.ed-wave-lane-row.is-selected { background: #eaf2fe; }
|
|
1955
|
-
.ed-wave-lane-handle {
|
|
1956
|
-
flex: 0 0 auto; cursor: move; color: #aaa; user-select: none; font-size: 13px;
|
|
1957
|
-
}
|
|
1958
|
-
.ed-wave-lane-handle:active { cursor: move; }
|
|
1959
|
-
/* min-width, not just flex-grow: min-width is what a flex item will not
|
|
1960
|
-
shrink past, and shrinking past it is the vertical-text failure above.
|
|
1961
|
-
MEASURED after this change (real page, 800x600, the journey suite's own
|
|
1962
|
-
wave fixture): rail 201px, handle 9.5px, badge 36px, and the field comes
|
|
1963
|
-
out 178.5px ungrouped, 162.5px one group deep, 136.5px on the {} spacer
|
|
1964
|
-
row that also carries the badge — writing-mode horizontal-tb on all six.
|
|
1965
|
-
So 120 is slack at every depth this fixture reaches, NOT a computed
|
|
1966
|
-
exact fit. Deeper nesting spends 16px a level and would eventually hit
|
|
1967
|
-
the floor; there the field stops shrinking and .ed-wave-lanes scrolls,
|
|
1968
|
-
which is the trade this rule chooses on purpose. */
|
|
1969
|
-
.ed-wave-lane-name {
|
|
1970
|
-
flex: 1 1 auto; min-width: 120px; font-size: 12px; padding: 2px 4px;
|
|
1971
|
-
}
|
|
1972
|
-
.ed-wave-lane-badge { flex: 0 0 auto; font-size: 10px; color: #888; }
|
|
1973
|
-
.ed-wave-group, .ed-wave-group-input {
|
|
1974
|
-
position: absolute; width: 14px; padding: 0; font-size: 11px;
|
|
1975
|
-
writing-mode: vertical-rl; text-orientation: mixed; overflow: hidden;
|
|
1976
|
-
border: 1px solid #d8d8d8; border-radius: 3px; background: #f2f2f2;
|
|
1977
|
-
color: #555; cursor: pointer; box-sizing: border-box;
|
|
1978
|
-
}
|
|
1979
|
-
.ed-wave-group-input { width: 16px; height: 90px; cursor: text; }
|
|
1980
|
-
/* position: relative so the data-label field below (v3.5.0 Task 11) has
|
|
1981
|
-
an offsetParent to measure from: wave-ui.js reads canvas.offsetLeft /
|
|
1982
|
-
offsetTop against THIS element, not a hard-coded copy of the padding
|
|
1983
|
-
number just below, so the two cannot drift apart if that padding ever
|
|
1984
|
-
changes. */
|
|
1985
|
-
.ed-wave-canvas-wrap { flex: 1 1 auto; overflow: auto; padding: 8px; position: relative; }
|
|
1986
|
-
.ed-wave-canvas { display: block; }
|
|
1987
|
-
/* v3.5.0 Task 11: the bus data-label field, positioned by wave-ui.js's
|
|
1988
|
-
openDataEdit() from geometry.cellRect() — left/top/width/height are all
|
|
1989
|
-
set inline per cell, so only the visual skin lives here. Sized to sit
|
|
1990
|
-
exactly over the box it labels, text centred to match .ed-wave-buslabel
|
|
1991
|
-
beside it in the drawing underneath, box-sizing so the border does not
|
|
1992
|
-
push the field past the cell it is meant to cover. */
|
|
1993
|
-
.ed-wave-data-input {
|
|
1994
|
-
position: absolute; z-index: 1; box-sizing: border-box; text-align: center;
|
|
1995
|
-
font-size: 11px; padding: 0 2px; border: 1px solid #1a73e8; border-radius: 2px;
|
|
1996
|
-
background: #fff; color: #222;
|
|
1997
|
-
}
|
|
1998
|
-
.ed-wave-grid { stroke: #ececec; stroke-width: 1; }
|
|
1999
|
-
.ed-wave-line, .ed-wave-edge { stroke: #111; stroke-width: 2; fill: none; }
|
|
2000
|
-
/* v3.6.0 Task 10: the toolbar glyph's plain-level backbone (0/1/h/l/z/u/d
|
|
2001
|
-
only -- bus/clock/x/gap glyphs draw their own shape with their own
|
|
2002
|
-
fixed canvas colour instead, further down). Declared after the rule
|
|
2003
|
-
above on purpose: both are single-class selectors, so source order is
|
|
2004
|
-
what lets currentColor win over the fixed #111 for this one case,
|
|
2005
|
-
turning the icon white together with the rest of the button's text
|
|
2006
|
-
under .ed-wave-brush.is-on. */
|
|
2023
|
+
border: 1px solid transparent; border-radius: 4px; background: transparent;
|
|
2024
|
+
color: #333; padding: 4px 8px; font-size: 13px; min-height: 32px; cursor: pointer;
|
|
2025
|
+
}
|
|
2026
|
+
.ed-wave-toolbar button:hover { background: var(--md-ed-hover); }
|
|
2027
|
+
.ed-wave-toolbar button:disabled { opacity: .45; cursor: default; background: transparent; }
|
|
2028
|
+
.ed-wave-sep { width: 1px; align-self: stretch; margin: 4px 4px; background: #e2e2e2; }
|
|
2029
|
+
.ed-wave-toolbar .ed-wave-done {
|
|
2030
|
+
margin-left: auto; background: #1a73e8; color: #fff; border-color: #1a73e8; padding: 4px 14px;
|
|
2031
|
+
}
|
|
2032
|
+
.ed-wave-toolbar .ed-wave-done:hover { background: #1a73e8; }
|
|
2033
|
+
/* A brush: the level's glyph with its keyboard character in the lower
|
|
2034
|
+
right corner (spec section 4.2). The glyph is wave-draw.js levelIcon,
|
|
2035
|
+
built from the same brick helpers that model the engine's bricks. The
|
|
2036
|
+
selector names the button type so it outranks the generic toolbar
|
|
2037
|
+
button rule above. */
|
|
2038
|
+
button.ed-wave-brush {
|
|
2039
|
+
position: relative; width: 38px; height: 34px; padding: 2px; box-sizing: border-box;
|
|
2040
|
+
border: 1px solid #c4c4c4; border-radius: 4px; background: #fff; font-family: monospace;
|
|
2041
|
+
display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
|
|
2042
|
+
}
|
|
2043
|
+
.ed-wave-brush-char {
|
|
2044
|
+
position: absolute; right: 2px; bottom: 1px; font-size: 9px; line-height: 1; color: #555;
|
|
2045
|
+
}
|
|
2046
|
+
.ed-wave-toolbar button.ed-wave-brush.is-on, .ed-wave-pop button.ed-wave-brush.is-on,
|
|
2047
|
+
.ed-wave-toolbar .ed-wave-brush-more.is-on, .ed-wave-toolbar .ed-wave-edge-arm.is-on {
|
|
2048
|
+
background: #1a73e8; color: #fff; border-color: #1a73e8;
|
|
2049
|
+
}
|
|
2050
|
+
.ed-wave-brush.is-on .ed-wave-brush-char { color: #fff; }
|
|
2051
|
+
.ed-wave-level-icon { display: block; overflow: visible; }
|
|
2052
|
+
/* The glyph's shapes: the plain-level backbone follows the button's text
|
|
2053
|
+
colour (so a picked brush turns white with its caption); bus, x, clock
|
|
2054
|
+
and gap glyphs carry their own canvas colours. */
|
|
2055
|
+
.ed-wave-line { stroke: #111; stroke-width: 2; fill: none; }
|
|
2007
2056
|
.ed-wave-level-icon-path { stroke: currentColor; }
|
|
2008
2057
|
.ed-wave-weak { stroke-dasharray: 4 3; }
|
|
2009
|
-
/* v3.5.0 Task 10: the P/N explicit-edge arrowhead — see wave-ui.js's
|
|
2010
|
-
drawLane isClock branch for the measurement behind its shape and
|
|
2011
|
-
placement. Filled, no stroke, matching the line colour above. */
|
|
2012
2058
|
.ed-wave-clock-arrow { fill: #111; stroke: none; }
|
|
2013
2059
|
.ed-wave-x { fill: #d9d9d9; stroke: #111; stroke-width: 2; }
|
|
2014
2060
|
.ed-wave-bus { fill: #f5e9c8; stroke: #111; stroke-width: 2; }
|
|
2015
2061
|
.ed-wave-bus-3 { fill: #d8ead3; }
|
|
2016
2062
|
.ed-wave-bus-4 { fill: #d6e4f7; }
|
|
2017
2063
|
.ed-wave-bus-5 { fill: #f7d6e4; }
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
.ed-wave-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
.ed-wave-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
.ed-wave-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
+
.ed-wave-gap { stroke: #111; stroke-width: 2; fill: none; }
|
|
2065
|
+
/* The one popover the toolbar has open at a time (wave-ui.js places it
|
|
2066
|
+
under its button): 更多's grouped brush grid, the 訊號 and 匯出 menus. */
|
|
2067
|
+
.ed-wave-pop {
|
|
2068
|
+
position: absolute; z-index: 4; background: #fff; border: 1px solid #e2e2e2;
|
|
2069
|
+
border-radius: 6px; box-shadow: 0 4px 16px var(--md-ed-shadow); padding: 6px;
|
|
2070
|
+
}
|
|
2071
|
+
.ed-wave-menu { display: flex; flex-direction: column; min-width: 140px; }
|
|
2072
|
+
.ed-wave-menu-item {
|
|
2073
|
+
border: 0; background: transparent; color: #333; text-align: left;
|
|
2074
|
+
padding: 6px 10px; font-size: 13px; border-radius: 4px; cursor: pointer;
|
|
2075
|
+
}
|
|
2076
|
+
.ed-wave-menu-item:hover, .ed-wave-menu-item:focus { background: var(--md-ed-hover); }
|
|
2077
|
+
/* Task 8: a menu item says the key its action also has (the grip menu's
|
|
2078
|
+
F2, Ctrl+D, Alt+arrows, Del) on its right; 刪除 is red. */
|
|
2079
|
+
.ed-wave-menu-item { display: flex; align-items: center; gap: 16px; }
|
|
2080
|
+
.ed-wave-menu-label { flex: 1 1 auto; }
|
|
2081
|
+
.ed-wave-menu-key { flex: 0 0 auto; font-size: 11px; color: #8a8a8a; }
|
|
2082
|
+
.ed-wave-menu-item.is-danger { color: var(--md-ed-err); }
|
|
2083
|
+
/* Task 8: the grip menu's 週期與相位 panel, period and phase of one lane. */
|
|
2084
|
+
.ed-wave-period-pop { display: flex; flex-direction: column; gap: 6px; }
|
|
2085
|
+
.ed-wave-period-row { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #333; }
|
|
2086
|
+
.ed-wave-period-cap { flex: 0 0 84px; }
|
|
2087
|
+
.ed-wave-period-input {
|
|
2088
|
+
width: 72px; box-sizing: border-box; padding: 3px 6px; font-size: 12px;
|
|
2089
|
+
border: 1px solid #e2e2e2; border-radius: 4px; background: #fff; color: #222;
|
|
2090
|
+
}
|
|
2091
|
+
/* Task 10 (spec section 4.6): the settings popover, the whole diagram.
|
|
2092
|
+
Title and cycle width on top, the overflow card under them, the rulers
|
|
2093
|
+
and the foot title folded under the advanced toggle. */
|
|
2094
|
+
.ed-wave-settings-pop { display: flex; flex-direction: column; gap: 6px; width: 260px; }
|
|
2095
|
+
.ed-wave-set-cap { font-size: 11px; color: #8a8a8a; margin-top: 2px; }
|
|
2096
|
+
.ed-wave-set-row { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #333; }
|
|
2097
|
+
.ed-wave-set-label { flex: 0 0 44px; }
|
|
2098
|
+
.ed-wave-set-input {
|
|
2099
|
+
flex: 1 1 auto; min-width: 0; box-sizing: border-box; padding: 3px 6px; font-size: 12px;
|
|
2100
|
+
font-family: inherit; border: 1px solid #e2e2e2; border-radius: 4px; background: #fff; color: #222;
|
|
2101
|
+
}
|
|
2102
|
+
textarea.ed-wave-set-input { resize: vertical; }
|
|
2103
|
+
.ed-wave-set-scales { display: flex; gap: 4px; }
|
|
2104
|
+
.ed-wave-set-scales button, .ed-wave-set-advanced, .ed-wave-set-fix {
|
|
2105
|
+
border: 1px solid #e2e2e2; border-radius: 4px; background: #fff; color: #333;
|
|
2106
|
+
padding: 3px 10px; font-size: 12px; cursor: pointer;
|
|
2107
|
+
}
|
|
2108
|
+
.ed-wave-set-scales button:hover, .ed-wave-set-advanced:hover, .ed-wave-set-fix:hover { background: var(--md-ed-hover); }
|
|
2109
|
+
.ed-wave-set-scales button[aria-pressed="true"] { background: #1a73e8; color: #fff; border-color: #1a73e8; }
|
|
2110
|
+
.ed-wave-set-advanced { align-self: flex-start; }
|
|
2111
|
+
.ed-wave-set-advanced::after { content: " ⌄"; }
|
|
2112
|
+
.ed-wave-set-advanced[aria-expanded="true"]::after { content: " ⌃"; }
|
|
2113
|
+
.ed-wave-set-adv { display: flex; flex-direction: column; gap: 6px; }
|
|
2114
|
+
.ed-wave-set-adv[hidden] { display: none; }
|
|
2115
|
+
.ed-wave-set-overflow {
|
|
2116
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 6px; padding: 6px 8px;
|
|
2117
|
+
border: 1px dashed var(--md-ed-dirty); border-radius: 4px; font-size: 12px; color: #333;
|
|
2118
|
+
}
|
|
2119
|
+
.ed-wave-set-overflow[hidden] { display: none; }
|
|
2120
|
+
.ed-wave-set-fix { color: var(--md-ed-dirty); border-color: var(--md-ed-dirty); }
|
|
2121
|
+
/* The export menu's last line: what every export looks like. */
|
|
2122
|
+
.ed-wave-export-note {
|
|
2123
|
+
border-top: 1px solid #e2e2e2; margin-top: 4px; padding: 6px 10px 2px; font-size: 11px; color: #8a8a8a;
|
|
2124
|
+
}
|
|
2125
|
+
/* The chip beside a label wider than its segment; one press widens the
|
|
2126
|
+
cycles. Above the layer, so a press on it never paints. */
|
|
2127
|
+
.ed-wave-overflow-chip {
|
|
2128
|
+
position: absolute; z-index: 2; white-space: nowrap; padding: 1px 6px; font-size: 11px;
|
|
2129
|
+
line-height: 14px; border: 1px solid var(--md-ed-dirty); border-radius: 8px;
|
|
2130
|
+
background: var(--md-ed-surface); color: var(--md-ed-dirty); cursor: pointer;
|
|
2131
|
+
}
|
|
2132
|
+
.ed-wave-overflow-chip[hidden] { display: none; }
|
|
2133
|
+
.ed-wave-brush-grid { display: flex; flex-direction: column; gap: 6px; }
|
|
2134
|
+
.ed-wave-brush-group { display: flex; align-items: center; gap: 8px; }
|
|
2135
|
+
.ed-wave-brush-group-title { flex: 0 0 32px; font-size: 11px; color: #8a8a8a; }
|
|
2136
|
+
.ed-wave-brushes { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
2137
|
+
/* Spec section 4.9: what the editor cannot place, right under the
|
|
2138
|
+
toolbar, in neutral colours, and it can be folded away. */
|
|
2139
|
+
.ed-wave-notice {
|
|
2140
|
+
display: flex; align-items: flex-start; gap: 8px; padding: 6px 12px;
|
|
2141
|
+
border-bottom: 1px solid #e2e2e2; background: #fafafa; color: #555; font-size: 12px;
|
|
2142
|
+
}
|
|
2143
|
+
.ed-wave-notice[hidden] { display: none; }
|
|
2144
|
+
.ed-wave-notice-text { flex: 1 1 auto; }
|
|
2145
|
+
.ed-wave-notice[data-folded] .ed-wave-notice-text {
|
|
2146
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
2147
|
+
}
|
|
2148
|
+
.ed-wave-notice-fold {
|
|
2149
|
+
flex: 0 0 auto; border: 0; background: transparent; color: #1a73e8;
|
|
2150
|
+
font-size: 12px; cursor: pointer; padding: 0 4px;
|
|
2151
|
+
}
|
|
2152
|
+
/* The stage: the canvas's scroll container, the dialog's whole width.
|
|
2153
|
+
position: relative so the data-label field (wave-ui.js openDataEdit)
|
|
2154
|
+
is placed in its scrolled content box. */
|
|
2155
|
+
.ed-wave-stage {
|
|
2156
|
+
flex: 1 1 auto; min-height: 0; overflow: auto; padding: 8px; position: relative;
|
|
2157
|
+
}
|
|
2158
|
+
/* The engine's svg and the interaction layer share this box. The top
|
|
2159
|
+
padding is the room for the layer's cycle ruler, which sits above the
|
|
2160
|
+
engine's drawing (wave-ui.js RULER_GAP is the baseline's distance). */
|
|
2161
|
+
.ed-wave-canvas-host { position: relative; display: inline-block; padding-top: 20px; }
|
|
2162
|
+
.ed-wave-canvas-error { color: #b00020; padding: 8px; }
|
|
2163
|
+
.ed-wave-canvas { display: block; }
|
|
2164
|
+
/* The drawing is in the Tab cycle, so it has to say when it holds the
|
|
2165
|
+
keyboard. */
|
|
2064
2166
|
.ed-wave-canvas:focus { outline: 2px solid #1a73e8; outline-offset: 2px; }
|
|
2065
|
-
/* The
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
.ed-wave-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
.ed-wave-
|
|
2086
|
-
.ed-wave-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
.ed-wave-edge-hit { stroke: transparent; stroke-width: 10; fill: none; pointer-events: stroke; }
|
|
2101
|
-
/* v3.5.0 Task 11 fix round 4 audit, closed in round 5: this text (drawn
|
|
2102
|
-
by renderEdges, AFTER .ed-wave-edge-hit) is the same shape of hazard as
|
|
2103
|
-
.ed-wave-selection and .ed-wave-gap above — purely decorative, sits on
|
|
2104
|
-
top of the fat invisible click target a press aimed at SELECTING the
|
|
2105
|
-
edge is meant to land on. Round 4 found this but could not apply
|
|
2106
|
-
pointer-events: none to the bare class: ed-wave-edge-label is not this
|
|
2107
|
-
text's own class alone — wave-ui.js's sidebar edgeLabelInput (the real,
|
|
2108
|
-
keyboard-and-mouse text field for editing an edge's label) uses the
|
|
2109
|
-
IDENTICAL class name, and a blanket rule would have also landed on
|
|
2110
|
-
that input field and blocked clicking into it with the mouse.
|
|
2111
|
-
Round 5's fix: scope the SELECTOR by element instead of renaming
|
|
2112
|
-
anything. text.ed-wave-edge-label only ever matches the SVG <text>
|
|
2113
|
-
wave-ui.js draws on the canvas — the sidebar control is an <input>, a
|
|
2114
|
-
different element entirely, and keeps its own default pointer-events
|
|
2115
|
-
untouched no matter what class it shares. */
|
|
2116
|
-
text.ed-wave-edge-label { pointer-events: none; }
|
|
2117
|
-
.ed-wave-edge-label { font-size: 11px; fill: #333; }
|
|
2118
|
-
/* v3.6.0 Task 8: the canvas's own name column — a mirror of the rail's
|
|
2119
|
-
.ed-wave-lane-name / .ed-wave-group (see the comment above
|
|
2120
|
-
.ed-wave-lane-rows). Unlike .ed-wave-edge-label these two class names
|
|
2121
|
-
are not shared with any non-SVG control, so pointer-events: none goes
|
|
2122
|
-
straight on the class rather than needing the element-scoped selector
|
|
2123
|
-
that rule uses. MEASURED (task-8-report.md): the longest lane name in
|
|
2124
|
-
this repo's fixtures is 12 chars ("he said \"hi\""), which at 11px in
|
|
2125
|
-
both the app's default sans-serif and the worst case (monospace)
|
|
2126
|
-
measures well under the 110px a top-level name has to fit in
|
|
2127
|
-
(nameColWidth 120 - NAME_GAP 10), so SIZES.nameColWidth stays 120. */
|
|
2128
|
-
.ed-wave-lane-label, .ed-wave-group-label { pointer-events: none; }
|
|
2129
|
-
.ed-wave-lane-label { font-size: 11px; fill: #333; }
|
|
2130
|
-
.ed-wave-group-label {
|
|
2131
|
-
font-size: 11px; fill: #555; writing-mode: vertical-rl; text-orientation: mixed;
|
|
2132
|
-
}
|
|
2133
|
-
/* v3.6.0 Task 9: the cycle ruler and the head/foot bands — see
|
|
2134
|
-
wave-draw.js's drawRuler()/drawBand() comment for the geometry (tick on
|
|
2135
|
-
boundaries, tock at centres, both filtered by every) and why this is
|
|
2136
|
-
read-only with respect to the document. Purely decorative like the name
|
|
2137
|
-
column above it, so pointer-events: none on the same principle. The
|
|
2138
|
-
ruler's own fill matches the pinned engine's own muted class (fill
|
|
2139
|
-
#aaa, node_modules/wavedrom's waveSkin CSS) so a tick/tock painted here
|
|
2140
|
-
reads the same shade as the identical mark in the WaveDrom preview
|
|
2141
|
-
beside it; the head/foot titles use the darker ed-wave-lane-label shade
|
|
2142
|
-
instead, matching the engine's own plain black fill for head.text /
|
|
2143
|
-
foot.text closely enough at this scale without introducing a third
|
|
2144
|
-
grey. */
|
|
2145
|
-
.ed-wave-ruler-tick, .ed-wave-ruler-tock, .ed-wave-head-text, .ed-wave-foot-text {
|
|
2146
|
-
pointer-events: none;
|
|
2167
|
+
/* The interaction layer's marks (wave-draw.js createLayer). None of them
|
|
2168
|
+
takes a press: the layer svg itself does, and the pointer is resolved
|
|
2169
|
+
to a cell by geometry. The exceptions are a transition dot, which takes
|
|
2170
|
+
it through its wider transparent ring, and the edges' hit twins below. */
|
|
2171
|
+
.ed-wave-ruler-num, .ed-wave-ruler-plus, .ed-wave-row-hover, .ed-wave-hover-col,
|
|
2172
|
+
.ed-wave-selection, .ed-wave-cursor, .ed-wave-dot-mark, .ed-wave-pending,
|
|
2173
|
+
.ed-wave-edge-sel, .ed-wave-edge-end, .ed-wave-overflow, .ed-wave-drop-line { pointer-events: none; }
|
|
2174
|
+
/* Task 8: where a lane dragged by its grip will land. */
|
|
2175
|
+
.ed-wave-drop-line { stroke: #1a73e8; stroke-width: 2; }
|
|
2176
|
+
.ed-wave-ruler-num { font-size: 11px; fill: #aaa; }
|
|
2177
|
+
.ed-wave-ruler-num.is-on { fill: #1a73e8; font-weight: bold; }
|
|
2178
|
+
.ed-wave-ruler-plus-glyph { font-size: 14px; font-weight: bold; fill: #1a73e8; }
|
|
2179
|
+
/* Task 7: the ruler's plus inserts a cycle where it stands, so its ring
|
|
2180
|
+
(wave-draw.js createLayer) is the one ruler mark that takes a press. */
|
|
2181
|
+
.ed-wave-ruler-plus-hit { pointer-events: all; cursor: pointer; }
|
|
2182
|
+
.ed-wave-row-hover, .ed-wave-hover-col { fill: var(--md-ed-sel); }
|
|
2183
|
+
.ed-wave-selection { fill: rgba(26,115,232,.18); stroke: #1a73e8; stroke-width: 1; }
|
|
2184
|
+
/* The keyboard's own cell, deliberately not the selection's mark: the
|
|
2185
|
+
selection is the range an operation acts on, the cursor the one cell
|
|
2186
|
+
the next keystroke paints, and both can be on screen at once. */
|
|
2187
|
+
.ed-wave-cursor { fill: none; stroke: #d93025; stroke-width: 2; }
|
|
2188
|
+
.ed-wave-dot { cursor: crosshair; }
|
|
2189
|
+
.ed-wave-dot-hit { pointer-events: all; }
|
|
2190
|
+
.ed-wave-dot-mark { fill: #fff; stroke: #1a73e8; stroke-width: 1.5; }
|
|
2191
|
+
.ed-wave-dot.is-hot .ed-wave-dot-mark { fill: #1a73e8; stroke: #0041c4; }
|
|
2192
|
+
.ed-wave-pending { stroke: #1a73e8; stroke-width: 2; stroke-dasharray: 5 4; fill: none; }
|
|
2193
|
+
.ed-wave-edge-sel { stroke: #1a73e8; stroke-width: 2; fill: none; }
|
|
2194
|
+
.ed-wave-edge-end { fill: #1a73e8; stroke: #fff; stroke-width: 1.5; }
|
|
2195
|
+
.ed-wave-overflow { fill: none; stroke: var(--md-ed-dirty); stroke-width: 1.5; stroke-dasharray: 3 2; }
|
|
2196
|
+
/* The bus data-label field, placed over its cell by openDataEdit; only
|
|
2197
|
+
the skin lives here. */
|
|
2198
|
+
.ed-wave-data-input {
|
|
2199
|
+
position: absolute; z-index: 1; box-sizing: border-box; text-align: center;
|
|
2200
|
+
font-size: 11px; padding: 0 2px; border: 1px solid #1a73e8; border-radius: 2px;
|
|
2201
|
+
background: #fff; color: #222;
|
|
2147
2202
|
}
|
|
2148
|
-
.
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2203
|
+
/* Task 8 (spec section 4.4): the name column. Each row and each group
|
|
2204
|
+
title has a grip, laid over the column by wave-ui.js (the box sits on
|
|
2205
|
+
the canvas host padding box, above the layer). A grip shows for the row
|
|
2206
|
+
the pointer is over, while it has the keyboard and while its menu is
|
|
2207
|
+
open; otherwise it is invisible and takes no press, so the column under
|
|
2208
|
+
it stays the name the pointer means. */
|
|
2209
|
+
.ed-wave-grips { position: absolute; left: 0; top: 0; width: 0; height: 0; z-index: 2; }
|
|
2210
|
+
.ed-wave-grip {
|
|
2211
|
+
position: absolute; width: 16px; height: 22px; padding: 0; margin: 0;
|
|
2212
|
+
display: flex; align-items: center; justify-content: center;
|
|
2213
|
+
border: 0; border-radius: 4px; background: var(--md-ed-surface); color: var(--md-ed-glyph);
|
|
2214
|
+
font-size: 13px; line-height: 1; cursor: pointer; opacity: 0; pointer-events: none;
|
|
2215
|
+
}
|
|
2216
|
+
.ed-wave-grip.is-on, .ed-wave-grip:focus, .ed-wave-grip[aria-expanded="true"] {
|
|
2217
|
+
opacity: 1; pointer-events: auto;
|
|
2218
|
+
}
|
|
2219
|
+
.ed-wave-grip:hover, .ed-wave-grip[aria-expanded="true"] { background: var(--md-ed-hover); color: #333; }
|
|
2220
|
+
.ed-wave-grip:focus { outline: 2px solid #1a73e8; outline-offset: 0; }
|
|
2221
|
+
/* The in-place name field over a signal name (right-aligned, like the
|
|
2222
|
+
name the engine draws) or a group title. */
|
|
2223
|
+
.ed-wave-rename {
|
|
2224
|
+
position: absolute; z-index: 3; box-sizing: border-box; text-align: right;
|
|
2225
|
+
font-size: 14px; padding: 0 6px; border: 1px solid #1a73e8; border-radius: 3px;
|
|
2226
|
+
background: #fff; color: #222;
|
|
2161
2227
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
.ed-wave-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
(
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
.ed-wave-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
}
|
|
2208
|
-
.ed-wave-
|
|
2209
|
-
.ed-wave-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
zero-size, unhittable body the whole time .ed-wave-side itself is
|
|
2220
|
-
collapsed to its <1100px 28px rail (below): 關聯線/文件/Lane all default
|
|
2221
|
-
data-open, and none of that changes just because the rail hides them.
|
|
2222
|
-
Left deliberately this way rather than making data-open track actual
|
|
2223
|
-
visibility — that would conflate "this section is open" with "the rail
|
|
2224
|
-
happens to be showing right now", and toggling ONE section would then
|
|
2225
|
-
have to silently relabel every OTHER already-open section's own state
|
|
2226
|
-
too. What actually closes the gap this could otherwise leave (a control
|
|
2227
|
-
that LOOKS reachable in the DOM but is not) is wave-ui.js's render()
|
|
2228
|
-
expanding the rail on the one transition that matters (selecting an
|
|
2229
|
-
edge) rather than this attribute pretending to mean something it does
|
|
2230
|
-
not. */
|
|
2231
|
-
.ed-wave-section[data-open] .ed-wave-section-body { display: block; }
|
|
2232
|
-
.ed-wave-section-body label { display: flex; align-items: center; gap: 6px; margin: 4px 0; }
|
|
2233
|
-
.ed-wave-section-body label span { flex: 0 0 96px; font-size: 12px; color: #555; }
|
|
2234
|
-
.ed-wave-section-body input {
|
|
2235
|
-
flex: 1 1 auto; min-width: 0; font-size: 12px; padding: 2px 4px;
|
|
2236
|
-
}
|
|
2237
|
-
.ed-wave-source {
|
|
2238
|
-
margin: 0; font-size: 11px; line-height: 1.45; white-space: pre;
|
|
2239
|
-
overflow: auto; max-height: 260px;
|
|
2240
|
-
}
|
|
2241
|
-
/* 視窗窄於這條線時右欄收成一條直立標籤,展開時覆蓋在畫布上 —— 否則
|
|
2242
|
-
800x600 的測試視窗下畫布會被 340px 固定寬的側欄吃掉大半,拖曳手勢會
|
|
2243
|
-
不可用(v3.6.0 Task 13 自己的 .ed-wave-lanes 註解量到的舊值:只剩
|
|
2244
|
-
282px)。收起時仍是普通文件流寬度(28px),展開時疊在畫布上方而不擠
|
|
2245
|
-
壓它。data-expanded 有三個來源:wave-panels.js 的 section() 點擊 title、
|
|
2246
|
-
同檔案的 collapseBtn 反過來移除它、以及 fix round 3 新增的
|
|
2247
|
-
wave-panels.js 的 expandSide()(由 wave-ui.js 的 render() 在
|
|
2248
|
-
selectedEdge 從 null 變成有值的那一拍呼叫——選取一條 edge 之後,
|
|
2249
|
-
整個回饋只長在這個側欄的關聯線面板裡,窄視窗下不展開就等於使用者
|
|
2250
|
-
什麼都看不到、也按不到)。 */
|
|
2251
|
-
/* v3.6.0 Task 13 fix round 1: the collapse-back button. Hidden everywhere
|
|
2252
|
-
except the one state it exists for — the <1100px rail, expanded — so it
|
|
2253
|
-
never shows, and never sits in the tab order, at a wide viewport or in
|
|
2254
|
-
the rail's own default collapsed state. */
|
|
2255
|
-
.ed-wave-side-collapse { display: none; }
|
|
2256
|
-
@media (max-width: 1100px) {
|
|
2257
|
-
.ed-wave-side { flex: 0 0 28px; padding: 4px 2px; }
|
|
2258
|
-
.ed-wave-side:not([data-expanded]) .ed-wave-section-body { display: none; }
|
|
2259
|
-
.ed-wave-side[data-expanded] {
|
|
2260
|
-
position: absolute; right: 0; top: 0; bottom: 0; width: 340px;
|
|
2261
|
-
background: #ffffff; z-index: 3; flex: 0 0 340px; padding: 8px;
|
|
2262
|
-
}
|
|
2263
|
-
.ed-wave-side[data-expanded] .ed-wave-side-collapse {
|
|
2264
|
-
display: block; width: 100%; text-align: right; border: 0;
|
|
2265
|
-
background: transparent; font-size: 12px; color: #555; cursor: pointer;
|
|
2266
|
-
padding: 2px 4px 6px;
|
|
2267
|
-
}
|
|
2228
|
+
.ed-wave-rename.is-group { text-align: left; }
|
|
2229
|
+
/* The add-a-signal row under the last lane. */
|
|
2230
|
+
.ed-wave-add-lane {
|
|
2231
|
+
display: block; margin: 4px 0 0; border: 0; border-radius: 4px; background: transparent;
|
|
2232
|
+
color: #1a73e8; font-size: 12px; padding: 4px 8px; cursor: pointer;
|
|
2233
|
+
}
|
|
2234
|
+
.ed-wave-add-lane:hover, .ed-wave-add-lane:focus { background: var(--md-ed-hover); }
|
|
2235
|
+
/* Spec section 4.3: the floating toolbar over a selected run of cycles,
|
|
2236
|
+
placed against the panel by wave-ui.js (above the run, or below it when
|
|
2237
|
+
there is no room). Under the popovers (z-index 4), so 電位's grid opens
|
|
2238
|
+
over it. */
|
|
2239
|
+
.ed-wave-range {
|
|
2240
|
+
position: absolute; z-index: 3; display: flex; align-items: center; gap: 2px;
|
|
2241
|
+
padding: 3px; background: #fff; border: 1px solid #e2e2e2; border-radius: 6px;
|
|
2242
|
+
box-shadow: 0 4px 16px var(--md-ed-shadow); white-space: nowrap;
|
|
2243
|
+
}
|
|
2244
|
+
.ed-wave-range button {
|
|
2245
|
+
border: 0; border-radius: 4px; background: transparent; color: #333;
|
|
2246
|
+
padding: 4px 8px; font-size: 12px; cursor: pointer;
|
|
2247
|
+
}
|
|
2248
|
+
.ed-wave-range button:hover, .ed-wave-range button:focus { background: var(--md-ed-hover); }
|
|
2249
|
+
/* Task 9 (spec section 4.5): the edges. The engine draws them under the
|
|
2250
|
+
layer, so the layer carries a transparent twin of each for a press to
|
|
2251
|
+
land on: the line (hit on its wide stroke only), the label box and an
|
|
2252
|
+
unlabelled self-loop ring (hit on their area). A selected edge has a
|
|
2253
|
+
ring at each end that takes a press, under its visible mark. The
|
|
2254
|
+
selection is drawn with the engine path and its matrix, so its width
|
|
2255
|
+
must not scale with it. */
|
|
2256
|
+
.ed-wave-edge-hit { pointer-events: stroke; cursor: pointer; }
|
|
2257
|
+
.ed-wave-edge-label-hit, .ed-wave-edge-loop-hit { pointer-events: all; }
|
|
2258
|
+
.ed-wave-edge-end-hit { pointer-events: all; cursor: move; }
|
|
2259
|
+
.ed-wave-edge-sel { vector-effect: non-scaling-stroke; }
|
|
2260
|
+
.ed-wave-note-sel { fill: none; stroke: #1a73e8; stroke-width: 1.5; pointer-events: none; }
|
|
2261
|
+
/* The toolbar beside a selected edge or note (right of its label),
|
|
2262
|
+
placed against the panel like the range toolbar; 更多 lists its shapes
|
|
2263
|
+
on a second row. */
|
|
2264
|
+
.ed-wave-edge-bar {
|
|
2265
|
+
position: absolute; z-index: 3; display: flex; flex-wrap: wrap; align-items: center; gap: 2px;
|
|
2266
|
+
max-width: 360px; padding: 3px; background: #fff; border: 1px solid #e2e2e2; border-radius: 6px;
|
|
2267
|
+
box-shadow: 0 4px 16px var(--md-ed-shadow); white-space: nowrap;
|
|
2268
|
+
}
|
|
2269
|
+
.ed-wave-edge-group { display: flex; gap: 1px; padding-right: 3px; margin-right: 1px; border-right: 1px solid #e2e2e2; }
|
|
2270
|
+
.ed-wave-edge-bar button {
|
|
2271
|
+
border: 0; border-radius: 4px; background: transparent; color: #333;
|
|
2272
|
+
padding: 4px 8px; font-size: 12px; cursor: pointer;
|
|
2273
|
+
}
|
|
2274
|
+
.ed-wave-edge-bar button:hover, .ed-wave-edge-bar button:focus { background: var(--md-ed-hover); }
|
|
2275
|
+
.ed-wave-edge-bar button[aria-pressed="true"] { background: #1a73e8; color: #fff; }
|
|
2276
|
+
.ed-wave-edge-bar button.is-danger { color: var(--md-ed-err); }
|
|
2277
|
+
.ed-wave-edge-more-list { display: flex; flex-wrap: wrap; gap: 1px; flex-basis: 100%; }
|
|
2278
|
+
.ed-wave-edge-more-list[hidden] { display: none; }
|
|
2279
|
+
.ed-wave-edge-more-list button { font-family: monospace; }
|
|
2280
|
+
/* The one-line field over an edge's label or a point note. */
|
|
2281
|
+
.ed-wave-edge-input {
|
|
2282
|
+
position: absolute; z-index: 3; box-sizing: border-box; text-align: center;
|
|
2283
|
+
font-size: 12px; padding: 0 4px; border: 1px solid #1a73e8; border-radius: 3px;
|
|
2284
|
+
background: #fff; color: #222;
|
|
2268
2285
|
}
|
|
2269
|
-
.
|
|
2270
|
-
|
|
2271
|
-
|
|
2286
|
+
/* Spec section 4.8: the bottom line says what can be done next, never
|
|
2287
|
+
what just happened (results go to the toast). Task 11: the sentence
|
|
2288
|
+
follows the state, and 「? 全部快捷鍵」 sits fixed at its right end. */
|
|
2289
|
+
.ed-wave-hint {
|
|
2290
|
+
display: flex; align-items: center; gap: 12px;
|
|
2291
|
+
border-top: 1px solid #e2e2e2; padding: 3px 8px 3px 12px; font-size: 12px; color: #444; min-height: 24px;
|
|
2292
|
+
}
|
|
2293
|
+
.ed-wave-hint-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2294
|
+
.ed-wave-hint-help {
|
|
2295
|
+
flex: 0 0 auto; border: 0; border-radius: 4px; background: transparent; color: #1a73e8;
|
|
2296
|
+
font-size: 12px; padding: 3px 8px; cursor: pointer;
|
|
2297
|
+
}
|
|
2298
|
+
.ed-wave-hint-help:hover, .ed-wave-hint-help[aria-expanded="true"] { background: var(--md-ed-hover); }
|
|
2299
|
+
/* Task 11: the shortcut card, in the middle of the editor (wave-ui.js
|
|
2300
|
+
centres it), above every other popover and floating toolbar. */
|
|
2301
|
+
.ed-wave-shortcuts {
|
|
2302
|
+
z-index: 5; box-sizing: border-box; width: min(600px, calc(100% - 16px));
|
|
2303
|
+
max-height: calc(100% - 16px); overflow: auto; padding: 10px 14px 14px;
|
|
2304
|
+
}
|
|
2305
|
+
.ed-wave-shortcuts:focus { outline: none; }
|
|
2306
|
+
.ed-wave-shortcuts-head { display: flex; align-items: center; margin-bottom: 4px; }
|
|
2307
|
+
.ed-wave-shortcuts-title { flex: 1 1 auto; font-weight: bold; font-size: 14px; color: #222; }
|
|
2308
|
+
.ed-wave-shortcuts-close {
|
|
2309
|
+
border: 0; border-radius: 4px; background: transparent; color: #555;
|
|
2310
|
+
width: 26px; height: 26px; font-size: 13px; cursor: pointer;
|
|
2311
|
+
}
|
|
2312
|
+
.ed-wave-shortcuts-close:hover { background: var(--md-ed-hover); }
|
|
2313
|
+
.ed-wave-shortcuts-body { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 24px; }
|
|
2314
|
+
.ed-wave-shortcut-title { font-weight: bold; font-size: 12px; color: #444; margin: 6px 0 4px; }
|
|
2315
|
+
.ed-wave-shortcut-row { display: flex; align-items: baseline; gap: 10px; font-size: 12px; color: #333; padding: 2px 0; }
|
|
2316
|
+
.ed-wave-shortcut-keys { flex: 0 0 92px; display: flex; flex-wrap: wrap; gap: 3px; }
|
|
2317
|
+
.ed-wave-shortcut-keys kbd {
|
|
2318
|
+
font-family: inherit; font-size: 11px; line-height: 16px; padding: 0 4px;
|
|
2319
|
+
border: 1px solid #e2e2e2; border-radius: 3px; background: #fff; color: #222; white-space: nowrap;
|
|
2320
|
+
}
|
|
2321
|
+
/* The cursor narration: for a screen reader only, so visually hidden but
|
|
2322
|
+
never display: none (a live region that is not rendered is not read). */
|
|
2323
|
+
.ed-wave-live {
|
|
2324
|
+
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
|
|
2325
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
2272
2326
|
}
|
|
2273
2327
|
.ed-wave-parse-error { padding: 16px; }
|
|
2274
2328
|
.ed-wave-parse-message { color: #b00020; font-weight: bold; }
|
|
@@ -2660,6 +2714,7 @@ const html = `<!DOCTYPE html>
|
|
|
2660
2714
|
padding: 0 2px;
|
|
2661
2715
|
border-radius: 3px;
|
|
2662
2716
|
}
|
|
2717
|
+
mark.search-hit-block { display: block; }
|
|
2663
2718
|
.toc {
|
|
2664
2719
|
display: flex;
|
|
2665
2720
|
flex-direction: column;
|
|
@@ -2795,6 +2850,12 @@ const html = `<!DOCTYPE html>
|
|
|
2795
2850
|
.toc-list-level-1 > .toc-item > details > summary > a {
|
|
2796
2851
|
font-size: 0.95em;
|
|
2797
2852
|
}
|
|
2853
|
+
/* v3.13.1 (R3): chapters of a multi-H1 TOC, set apart by a rule and a gap; weight unchanged. */
|
|
2854
|
+
.toc[data-toc-chapters] > .toc-list-level-1 > .toc-item + .toc-item {
|
|
2855
|
+
margin-top: 10px;
|
|
2856
|
+
padding-top: 6px;
|
|
2857
|
+
border-top: 1px solid #d8dee4;
|
|
2858
|
+
}
|
|
2798
2859
|
.toc-list-level-2 > .toc-item > a,
|
|
2799
2860
|
.toc-list-level-2 > .toc-item > details > summary > a {
|
|
2800
2861
|
font-size: 0.9em;
|
|
@@ -4116,6 +4177,7 @@ ${mermaidInitTag}`)
|
|
|
4116
4177
|
results: [],
|
|
4117
4178
|
selectedResultIndex: -1,
|
|
4118
4179
|
activeHighlight: null,
|
|
4180
|
+
activeHighlights: [],
|
|
4119
4181
|
};
|
|
4120
4182
|
window.__readerState = readerState;
|
|
4121
4183
|
|
|
@@ -4258,14 +4320,18 @@ ${mermaidInitTag}`)
|
|
|
4258
4320
|
// already above the line and would steal the highlight from the row that was
|
|
4259
4321
|
// just clicked. The clicked heading stays the answer for as long as it is
|
|
4260
4322
|
// still between the page top and the line, and the first scroll that moves
|
|
4261
|
-
// it out of that band hands tracking back to the line.
|
|
4323
|
+
// it out of that band hands tracking back to the line. The last sections of
|
|
4324
|
+
// a document can be too short to ever reach the line: at the bottom of the
|
|
4325
|
+
// page a clicked heading anywhere on screen keeps its row lit. Only a click
|
|
4326
|
+
// pins, so a plain scroll to the bottom still follows the line.
|
|
4262
4327
|
let pinnedHeadingId = null;
|
|
4263
4328
|
function readingLineHeading() {
|
|
4264
4329
|
const line = window.innerHeight * 0.35;
|
|
4265
4330
|
if (pinnedHeadingId) {
|
|
4266
4331
|
const pinned = document.getElementById(pinnedHeadingId);
|
|
4267
4332
|
const top = pinned ? pinned.getBoundingClientRect().top : Infinity;
|
|
4268
|
-
|
|
4333
|
+
const atBottom = window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;
|
|
4334
|
+
if (pinned && tocLinks.has(pinned.id) && top >= -40 && (top <= line || (atBottom && top < window.innerHeight))) return pinned;
|
|
4269
4335
|
pinnedHeadingId = null;
|
|
4270
4336
|
}
|
|
4271
4337
|
let first = null;
|
|
@@ -4304,15 +4370,54 @@ ${mermaidInitTag}`)
|
|
|
4304
4370
|
syncActiveHeading(node.id);
|
|
4305
4371
|
syncReadingTitle(node);
|
|
4306
4372
|
}
|
|
4307
|
-
|
|
4373
|
+
// Scrolls are not the only thing that moves a heading across the line: a
|
|
4374
|
+
// height-only window resize moves the line itself (and reflows nothing, so
|
|
4375
|
+
// no scroll follows), and a column resize reflows the text under it.
|
|
4376
|
+
function scheduleReadingLinePass() {
|
|
4308
4377
|
if (readingLinePending) return;
|
|
4309
4378
|
readingLinePending = true;
|
|
4310
4379
|
requestAnimationFrame(readingLinePass);
|
|
4311
|
-
}
|
|
4380
|
+
}
|
|
4381
|
+
window.addEventListener('scroll', scheduleReadingLinePass, { passive: true });
|
|
4382
|
+
window.addEventListener('resize', scheduleReadingLinePass);
|
|
4383
|
+
// Ends the freeze a selected search result put on the highlight. The freeze
|
|
4384
|
+
// also kept the TOC from opening the hit's path, and the next pass may find
|
|
4385
|
+
// the same section (the reader is still inside the hit's section), which
|
|
4386
|
+
// syncActiveHeading skips as no change: so the next frame re-syncs the row
|
|
4387
|
+
// itself, opening its path and centring it.
|
|
4388
|
+
function endSearchFreeze() {
|
|
4389
|
+
observerFrozen = false;
|
|
4390
|
+
requestAnimationFrame(resyncTocToScroll);
|
|
4391
|
+
}
|
|
4392
|
+
// A TOC click is the reader leaving a search hit: it also ends the freeze,
|
|
4393
|
+
// so the clicked row lights.
|
|
4312
4394
|
document.addEventListener('click', (event) => {
|
|
4313
4395
|
const link = event.target && event.target.closest && event.target.closest('.toc a[href^="#"]');
|
|
4314
|
-
if (link)
|
|
4396
|
+
if (!link) return;
|
|
4397
|
+
pinnedHeadingId = link.getAttribute('href').slice(1);
|
|
4398
|
+
endSearchFreeze();
|
|
4315
4399
|
});
|
|
4400
|
+
// So does a scroll of the page by hand (R6b, R17): a wheel, a touch drag or
|
|
4401
|
+
// a scroll key. Inside the sidebar (the results list, the TOC) or the
|
|
4402
|
+
// lightbox the same gestures work that control, and keep the freeze; so do
|
|
4403
|
+
// a sideways wheel (a wide table's swipe), ctrl+wheel (zoom) and a key typed
|
|
4404
|
+
// into a field. A programmatic scroll (the result jump itself) fires none of
|
|
4405
|
+
// these events.
|
|
4406
|
+
const SCROLL_KEYS = new Set(['PageUp', 'PageDown', 'Home', 'End', 'ArrowUp', 'ArrowDown', ' ', 'Spacebar']);
|
|
4407
|
+
const unfreezeOnUserScroll = (event) => {
|
|
4408
|
+
if (!observerFrozen) return;
|
|
4409
|
+
const target = event.target;
|
|
4410
|
+
if (event.type === 'wheel' && (event.deltaY === 0 || event.ctrlKey)) return;
|
|
4411
|
+
if (event.type === 'keydown') {
|
|
4412
|
+
if (!SCROLL_KEYS.has(event.key) || lightboxIsOpen()) return;
|
|
4413
|
+
if (target instanceof Element && (target.closest('input, textarea, select') || target.isContentEditable)) return;
|
|
4414
|
+
}
|
|
4415
|
+
if (target instanceof Element && target.closest('.reader-sidebar, .lightbox')) return;
|
|
4416
|
+
endSearchFreeze();
|
|
4417
|
+
};
|
|
4418
|
+
window.addEventListener('wheel', unfreezeOnUserScroll, { passive: true });
|
|
4419
|
+
window.addEventListener('touchmove', unfreezeOnUserScroll, { passive: true });
|
|
4420
|
+
window.addEventListener('keydown', unfreezeOnUserScroll);
|
|
4316
4421
|
function bindHeadingObserver() {
|
|
4317
4422
|
readingLinePass();
|
|
4318
4423
|
}
|
|
@@ -4417,13 +4522,22 @@ ${mermaidInitTag}`)
|
|
|
4417
4522
|
});
|
|
4418
4523
|
}
|
|
4419
4524
|
|
|
4525
|
+
// A hit that crosses inline markup is several marks (see
|
|
4526
|
+
// highlightFirstOccurrence); each is unwrapped and its own parent normalised,
|
|
4527
|
+
// which rejoins the text it was cut from, so main.content is byte-identical.
|
|
4528
|
+
// A math mark wraps a whole formula and cut nothing, so it is only unwrapped.
|
|
4420
4529
|
function clearSelectedHighlight() {
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4530
|
+
const marks = readerState.activeHighlights.length
|
|
4531
|
+
? readerState.activeHighlights
|
|
4532
|
+
: (readerState.activeHighlight ? [readerState.activeHighlight] : []);
|
|
4533
|
+
marks.forEach((mark) => {
|
|
4534
|
+
const parent = mark.parentNode;
|
|
4535
|
+
if (!parent) return;
|
|
4536
|
+
while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
|
|
4537
|
+
parent.removeChild(mark);
|
|
4538
|
+
if (!mark.classList.contains('search-hit-math')) parent.normalize();
|
|
4539
|
+
});
|
|
4540
|
+
readerState.activeHighlights = [];
|
|
4427
4541
|
readerState.activeHighlight = null;
|
|
4428
4542
|
}
|
|
4429
4543
|
|
|
@@ -4489,61 +4603,153 @@ ${mermaidInitTag}`)
|
|
|
4489
4603
|
return { start, end };
|
|
4490
4604
|
}
|
|
4491
4605
|
|
|
4606
|
+
// A whitespace-only text node is the real space between words when it sits in
|
|
4607
|
+
// an inline element or between two inline siblings ("**bold** *italic*").
|
|
4608
|
+
// Between blocks (and inside list / table structure) it is markup layout:
|
|
4609
|
+
// block boundaries get a virtual space instead, and a mark must never land
|
|
4610
|
+
// there (a mark as a direct child of a ul or tr).
|
|
4611
|
+
const INLINE_SELECTOR = 'a, abbr, b, bdi, bdo, cite, code, data, del, dfn, em, i, img, ins, kbd, mark, q, s, samp, small, span, strong, sub, sup, time, u, var';
|
|
4612
|
+
function isInlineSibling(node) {
|
|
4613
|
+
if (!node) return false;
|
|
4614
|
+
if (node.nodeType === Node.TEXT_NODE) return !!node.nodeValue.trim();
|
|
4615
|
+
return node.nodeType === Node.ELEMENT_NODE && node.matches(INLINE_SELECTOR);
|
|
4616
|
+
}
|
|
4617
|
+
function isInlineSpace(node) {
|
|
4618
|
+
const parent = node.parentElement;
|
|
4619
|
+
if (!parent) return false;
|
|
4620
|
+
if (parent.matches(INLINE_SELECTOR)) return true;
|
|
4621
|
+
return isInlineSibling(node.previousSibling) && isInlineSibling(node.nextSibling);
|
|
4622
|
+
}
|
|
4623
|
+
|
|
4624
|
+
// The section's text as one string, read the way the search index reads it
|
|
4625
|
+
// (renderer side, sectionInlineText): whitespace runs fold to one space and
|
|
4626
|
+
// letters to lower case, as normalizeText folds the query; a br element is a
|
|
4627
|
+
// space; a formula (.katex) is its TeX source, taken from its annotation, and
|
|
4628
|
+
// its MathML and glyph markup are never walked; two blocks are joined by a
|
|
4629
|
+
// space. Every flat position maps back to its (node, offset), so a phrase can
|
|
4630
|
+
// cross a heading's .sec number, inline code, bold, a soft or hard line break
|
|
4631
|
+
// or a formula. Spaces that stand for a br or a block boundary map to no node
|
|
4632
|
+
// and are never wrapped. The walk stops at the first node that completes a
|
|
4633
|
+
// hit, so a long section is not read past its first occurrence.
|
|
4492
4634
|
function highlightFirstOccurrence(sectionId, query) {
|
|
4493
4635
|
const bounds = sectionBoundary(sectionId);
|
|
4494
4636
|
if (!bounds) return null;
|
|
4495
4637
|
const { start, end } = bounds;
|
|
4496
4638
|
const container = start.parentNode;
|
|
4497
4639
|
if (!container) return null;
|
|
4498
|
-
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
|
|
4640
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, {
|
|
4499
4641
|
acceptNode(node) {
|
|
4500
|
-
if (
|
|
4501
|
-
|
|
4502
|
-
|
|
4642
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
4643
|
+
if (node.matches(SKIP_SELECTOR) || node.matches('.reader-sidebar, .heading-anchor')) return NodeFilter.FILTER_REJECT;
|
|
4644
|
+
return node.matches('br, .katex') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
4645
|
+
}
|
|
4646
|
+
if (!node.nodeValue) return NodeFilter.FILTER_REJECT;
|
|
4647
|
+
if (!node.nodeValue.trim() && !isInlineSpace(node)) return NodeFilter.FILTER_REJECT;
|
|
4503
4648
|
return NodeFilter.FILTER_ACCEPT;
|
|
4504
4649
|
},
|
|
4505
4650
|
});
|
|
4506
|
-
|
|
4507
|
-
|
|
4651
|
+
// Nothing before the heading is in range, so the walk starts at it.
|
|
4652
|
+
walker.currentNode = start;
|
|
4653
|
+
const BLOCK = 'p, li, td, th, h1, h2, h3, h4, h5, h6, pre, blockquote, dt, dd, figcaption, .katex-display';
|
|
4654
|
+
const nodes = [];
|
|
4655
|
+
let flat = '';
|
|
4656
|
+
const at = [];
|
|
4657
|
+
let space = true;
|
|
4658
|
+
let lastBlock = null;
|
|
4659
|
+
let idx = -1;
|
|
4508
4660
|
while (walker.nextNode()) {
|
|
4509
4661
|
const node = walker.currentNode;
|
|
4510
|
-
if (!inRange) {
|
|
4511
|
-
if (start.contains(node)) {
|
|
4512
|
-
inRange = true;
|
|
4513
|
-
} else if (start.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
4514
|
-
inRange = true;
|
|
4515
|
-
} else {
|
|
4516
|
-
continue;
|
|
4517
|
-
}
|
|
4518
|
-
}
|
|
4519
|
-
if (node.parentElement && node.parentElement.closest('.heading-anchor')) continue;
|
|
4520
4662
|
if (end) {
|
|
4521
4663
|
const pos = end.compareDocumentPosition(node);
|
|
4522
4664
|
if (end === node || end.contains(node) || (pos & Node.DOCUMENT_POSITION_FOLLOWING)) {
|
|
4523
4665
|
break;
|
|
4524
4666
|
}
|
|
4525
4667
|
}
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4668
|
+
if (node.nodeName === 'BR') {
|
|
4669
|
+
if (!space) {
|
|
4670
|
+
flat += ' ';
|
|
4671
|
+
at.push(null);
|
|
4672
|
+
space = true;
|
|
4673
|
+
}
|
|
4674
|
+
continue;
|
|
4675
|
+
}
|
|
4676
|
+
const isMath = node.nodeType === Node.ELEMENT_NODE;
|
|
4677
|
+
let text = node.nodeValue;
|
|
4678
|
+
if (isMath) {
|
|
4679
|
+
const tex = node.querySelector('annotation[encoding="application/x-tex"]');
|
|
4680
|
+
text = tex ? tex.textContent : '';
|
|
4681
|
+
// Resume the walk after the formula's last descendant.
|
|
4682
|
+
let last = node;
|
|
4683
|
+
while (last.lastChild) last = last.lastChild;
|
|
4684
|
+
walker.currentNode = last;
|
|
4685
|
+
}
|
|
4686
|
+
const ni = nodes.length;
|
|
4687
|
+
nodes.push(node);
|
|
4688
|
+
const prevLen = flat.length;
|
|
4689
|
+
const block = (node.parentElement && node.parentElement.closest(BLOCK)) || (isMath ? node : null);
|
|
4690
|
+
if (ni && block !== lastBlock && !space) {
|
|
4691
|
+
flat += ' ';
|
|
4692
|
+
at.push(null);
|
|
4693
|
+
space = true;
|
|
4694
|
+
}
|
|
4695
|
+
lastBlock = block;
|
|
4696
|
+
for (let i = 0; i < text.length; i++) {
|
|
4697
|
+
if (/\\s/.test(text[i])) {
|
|
4698
|
+
if (space) continue;
|
|
4699
|
+
flat += ' ';
|
|
4700
|
+
at.push([ni, i]);
|
|
4701
|
+
space = true;
|
|
4702
|
+
} else {
|
|
4703
|
+
// A few characters lower-case to two code units; map each to i.
|
|
4704
|
+
const lower = text[i].toLowerCase();
|
|
4705
|
+
flat += lower;
|
|
4706
|
+
for (let u = 0; u < lower.length; u++) at.push([ni, i]);
|
|
4707
|
+
space = false;
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
// Only a hit that ends in this node's text can be new.
|
|
4711
|
+
idx = flat.indexOf(query, Math.max(0, prevLen - query.length + 1));
|
|
4712
|
+
if (idx !== -1) break;
|
|
4713
|
+
}
|
|
4714
|
+
if (idx === -1) return null;
|
|
4715
|
+
// node index -> [first offset, last offset] of the hit inside that node
|
|
4716
|
+
const spans = new Map();
|
|
4717
|
+
for (let k = idx; k < idx + query.length; k++) {
|
|
4718
|
+
const p = at[k];
|
|
4719
|
+
if (!p) continue;
|
|
4720
|
+
const span = spans.get(p[0]) || [p[1], p[1]];
|
|
4721
|
+
span[1] = p[1];
|
|
4722
|
+
spans.set(p[0], span);
|
|
4723
|
+
}
|
|
4724
|
+
// One mark per node, wrapping only that node's slice, so the .sec / code /
|
|
4725
|
+
// bold wrappers stay whole siblings and clearing restores the markup
|
|
4726
|
+
// exactly. Last node first; each node is split only once anyway. A formula
|
|
4727
|
+
// is marked whole from outside, so KaTeX's own markup is never cut; a
|
|
4728
|
+
// display formula's mark goes around its .katex-display (KaTeX styles
|
|
4729
|
+
// .katex-display > .katex) and is a block, so its background shows.
|
|
4730
|
+
const marks = [];
|
|
4731
|
+
Array.from(spans.keys()).sort((a, b) => b - a).forEach((ni) => {
|
|
4732
|
+
const node = nodes[ni];
|
|
4532
4733
|
const mark = document.createElement('mark');
|
|
4533
4734
|
mark.className = 'search-hit is-selected';
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4735
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
4736
|
+
const display = node.parentElement && node.parentElement.classList.contains('katex-display') ? node.parentElement : null;
|
|
4737
|
+
const target = display || node;
|
|
4738
|
+
mark.classList.add('search-hit-math');
|
|
4739
|
+
if (display) mark.classList.add('search-hit-block');
|
|
4740
|
+
target.parentNode.insertBefore(mark, target);
|
|
4741
|
+
mark.appendChild(target);
|
|
4541
4742
|
} else {
|
|
4542
|
-
|
|
4743
|
+
const span = spans.get(ni);
|
|
4744
|
+
const range = document.createRange();
|
|
4745
|
+
range.setStart(node, span[0]);
|
|
4746
|
+
range.setEnd(node, span[1] + 1);
|
|
4747
|
+
range.surroundContents(mark);
|
|
4543
4748
|
}
|
|
4544
|
-
|
|
4545
|
-
}
|
|
4546
|
-
|
|
4749
|
+
marks.unshift(mark);
|
|
4750
|
+
});
|
|
4751
|
+
readerState.activeHighlights = marks;
|
|
4752
|
+
return marks[0] || null;
|
|
4547
4753
|
}
|
|
4548
4754
|
|
|
4549
4755
|
function jumpToAndHighlight(result) {
|
|
@@ -4791,6 +4997,11 @@ ${mermaidInitTag}`)
|
|
|
4791
4997
|
var anchorTop = 0;
|
|
4792
4998
|
var anchorFrame = 0;
|
|
4793
4999
|
var suppressAnchorCapture = false;
|
|
5000
|
+
// Set by any scroll that is not our own restore, cleared by a capture: the
|
|
5001
|
+
// anchor then describes a position the page has already left (R1, R9).
|
|
5002
|
+
var anchorStale = false;
|
|
5003
|
+
// scrollY when the anchor was captured or last restored by us.
|
|
5004
|
+
var anchorScrollY = 0;
|
|
4794
5005
|
// Column width the anchor was last captured/held at; -1 until the column
|
|
4795
5006
|
// ResizeObserver below is installed. See captureScrollAnchor's first guard.
|
|
4796
5007
|
var lastAnchorColW = -1;
|
|
@@ -4836,8 +5047,13 @@ ${mermaidInitTag}`)
|
|
|
4836
5047
|
}
|
|
4837
5048
|
}
|
|
4838
5049
|
if (!node) return;
|
|
5050
|
+
// A display formula's search mark is itself a child of the column, and
|
|
5051
|
+
// clearing the search removes it: pin the formula inside it instead.
|
|
5052
|
+
if (node.matches('mark.search-hit') && node.firstElementChild) node = node.firstElementChild;
|
|
4839
5053
|
anchorNode = node;
|
|
4840
5054
|
anchorTop = node.getBoundingClientRect().top;
|
|
5055
|
+
anchorScrollY = window.scrollY;
|
|
5056
|
+
anchorStale = false;
|
|
4841
5057
|
}
|
|
4842
5058
|
|
|
4843
5059
|
function restoreScrollAnchor() {
|
|
@@ -4845,9 +5061,13 @@ ${mermaidInitTag}`)
|
|
|
4845
5061
|
var delta = anchorNode.getBoundingClientRect().top - anchorTop;
|
|
4846
5062
|
if (!delta) return;
|
|
4847
5063
|
window.scrollTo(window.scrollX, window.scrollY + delta);
|
|
5064
|
+
// Our own correction is not a newer scroll: without this, the next frame
|
|
5065
|
+
// of a splitter drag reads it as one and drops the anchor.
|
|
5066
|
+
anchorScrollY = window.scrollY;
|
|
4848
5067
|
}
|
|
4849
5068
|
|
|
4850
5069
|
window.addEventListener('scroll', function () {
|
|
5070
|
+
if (!suppressAnchorCapture) anchorStale = true;
|
|
4851
5071
|
if (anchorFrame) return;
|
|
4852
5072
|
anchorFrame = window.requestAnimationFrame(function () {
|
|
4853
5073
|
anchorFrame = 0;
|
|
@@ -4856,6 +5076,13 @@ ${mermaidInitTag}`)
|
|
|
4856
5076
|
}, { passive: true });
|
|
4857
5077
|
|
|
4858
5078
|
function holdScrollAnchor() {
|
|
5079
|
+
// A scroll issued before this reflow's frame may not have dispatched its
|
|
5080
|
+
// event yet (resize runs before scroll in a frame): compare offsets too.
|
|
5081
|
+
// A shorter document clamping the offset is not somebody else's scroll.
|
|
5082
|
+
var maxY = document.documentElement.scrollHeight - window.innerHeight;
|
|
5083
|
+
var moved = Math.abs(window.scrollY - anchorScrollY) > 1;
|
|
5084
|
+
var clamped = window.scrollY < anchorScrollY && window.scrollY >= maxY - 1;
|
|
5085
|
+
if (anchorStale || (moved && !clamped)) anchorNode = null;
|
|
4859
5086
|
// Hold the anchor across the whole reflow: the scrollTo below fires scroll
|
|
4860
5087
|
// events that would otherwise re-capture a mid-reflow position.
|
|
4861
5088
|
suppressAnchorCapture = true;
|
|
@@ -4895,6 +5122,7 @@ ${mermaidInitTag}`)
|
|
|
4895
5122
|
if (w === lastAnchorColW) return;
|
|
4896
5123
|
lastAnchorColW = w;
|
|
4897
5124
|
holdScrollAnchor();
|
|
5125
|
+
scheduleReadingLinePass();
|
|
4898
5126
|
}).observe(contentEl);
|
|
4899
5127
|
}
|
|
4900
5128
|
|
|
@@ -6377,6 +6605,9 @@ if (require.main === module) {
|
|
|
6377
6605
|
const [,, src, dst] = process.argv;
|
|
6378
6606
|
const BAKE_SVG = process.argv.slice(4).includes('--bake-svg');
|
|
6379
6607
|
const LANG_ARG = (process.argv.slice(4).find((a) => a.startsWith('--lang=')) || '').slice('--lang='.length) || undefined;
|
|
6608
|
+
// bin/md2doc.js renders each format of one input in its own process and
|
|
6609
|
+
// passes this to every one after the first, so --html --pdf warns once.
|
|
6610
|
+
const WAVE_WARN = !process.argv.slice(4).includes('--no-wave-warn');
|
|
6380
6611
|
if (!src || !dst) {
|
|
6381
6612
|
console.error('Usage: node md2doc.js <input.md> <output.html|pdf>');
|
|
6382
6613
|
process.exit(1);
|
|
@@ -6386,7 +6617,7 @@ if (require.main === module) {
|
|
|
6386
6617
|
|
|
6387
6618
|
(async () => {
|
|
6388
6619
|
const mdText = fs.readFileSync(src, 'utf8');
|
|
6389
|
-
const { html } = await renderMarkdown(mdText, path.resolve(src), { lang: LANG_ARG });
|
|
6620
|
+
const { html } = await renderMarkdown(mdText, path.resolve(src), { lang: LANG_ARG, warnWaveOverflow: WAVE_WARN ? src : undefined });
|
|
6390
6621
|
let finalHtml = html;
|
|
6391
6622
|
|
|
6392
6623
|
if (ext === '.html') {
|