@vectojs/markdown 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Markdown.d.ts +96 -8
- package/dist/blockAffordances.d.ts +60 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +635 -71
- package/dist/index.mjs +633 -71
- package/dist/markdown-code.d.ts +136 -5
- package/dist/theme.d.ts +21 -0
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -554,11 +554,13 @@ var DEFAULT_THEME = {
|
|
|
554
554
|
syntaxStringColor: "#86efac",
|
|
555
555
|
syntaxCommentColor: "#64748b",
|
|
556
556
|
syntaxNumberColor: "#fbbf24",
|
|
557
|
+
codeLangColor: "#64748b",
|
|
557
558
|
bodyFont: "Inter, system-ui, sans-serif",
|
|
558
559
|
codeFont: 'ui-monospace, "JetBrains Mono", "Fira Code", monospace',
|
|
559
560
|
fontSize: 16,
|
|
560
561
|
headingSizes: [32, 28, 24, 20, 18, 16],
|
|
561
562
|
codeFontSize: 15,
|
|
563
|
+
codeLangFontSize: 12,
|
|
562
564
|
tableFontSize: 14,
|
|
563
565
|
footnoteMarkerScale: 0.75,
|
|
564
566
|
subscriptScale: 0.75,
|
|
@@ -593,6 +595,12 @@ function resolveTheme(theme) {
|
|
|
593
595
|
if (theme?.footnoteColor === void 0) {
|
|
594
596
|
merged.footnoteColor = merged.linkColor;
|
|
595
597
|
}
|
|
598
|
+
if (theme?.codeLangColor === void 0) {
|
|
599
|
+
merged.codeLangColor = merged.syntaxCommentColor;
|
|
600
|
+
}
|
|
601
|
+
if (theme?.codeLangFontSize === void 0) {
|
|
602
|
+
merged.codeLangFontSize = Math.max(1, merged.codeFontSize - 3);
|
|
603
|
+
}
|
|
596
604
|
return merged;
|
|
597
605
|
}
|
|
598
606
|
function headingSize(theme, depth) {
|
|
@@ -932,14 +940,215 @@ var KEYWORD_SETS = {
|
|
|
932
940
|
"static"
|
|
933
941
|
])
|
|
934
942
|
};
|
|
943
|
+
KEYWORD_SETS["bash"] = /* @__PURE__ */ new Set([
|
|
944
|
+
// Shell builtins and control words. Deliberately not the whole of coreutils:
|
|
945
|
+
// a keyword table that includes every command name colors an entire script
|
|
946
|
+
// uniformly, which reads worse than coloring only the control flow.
|
|
947
|
+
"if",
|
|
948
|
+
"then",
|
|
949
|
+
"else",
|
|
950
|
+
"elif",
|
|
951
|
+
"fi",
|
|
952
|
+
"for",
|
|
953
|
+
"while",
|
|
954
|
+
"until",
|
|
955
|
+
"do",
|
|
956
|
+
"done",
|
|
957
|
+
"case",
|
|
958
|
+
"esac",
|
|
959
|
+
"in",
|
|
960
|
+
"function",
|
|
961
|
+
"return",
|
|
962
|
+
"exit",
|
|
963
|
+
"break",
|
|
964
|
+
"continue",
|
|
965
|
+
"local",
|
|
966
|
+
"export",
|
|
967
|
+
"readonly",
|
|
968
|
+
"declare",
|
|
969
|
+
"unset",
|
|
970
|
+
"shift",
|
|
971
|
+
"source",
|
|
972
|
+
"alias",
|
|
973
|
+
"set",
|
|
974
|
+
"trap",
|
|
975
|
+
"echo",
|
|
976
|
+
"cd",
|
|
977
|
+
"sudo",
|
|
978
|
+
"true",
|
|
979
|
+
"false"
|
|
980
|
+
]);
|
|
981
|
+
KEYWORD_SETS["json"] = /* @__PURE__ */ new Set(["true", "false", "null"]);
|
|
982
|
+
KEYWORD_SETS["css"] = /* @__PURE__ */ new Set([
|
|
983
|
+
"important",
|
|
984
|
+
"inherit",
|
|
985
|
+
"initial",
|
|
986
|
+
"unset",
|
|
987
|
+
"revert",
|
|
988
|
+
"auto",
|
|
989
|
+
"none",
|
|
990
|
+
"var",
|
|
991
|
+
"calc"
|
|
992
|
+
]);
|
|
993
|
+
KEYWORD_SETS["html"] = /* @__PURE__ */ new Set([
|
|
994
|
+
// Tag names are the meaningful tokens a reader scans for. The tokenizer is
|
|
995
|
+
// word-based, so `<div>` yields the word `div`.
|
|
996
|
+
"html",
|
|
997
|
+
"head",
|
|
998
|
+
"body",
|
|
999
|
+
"title",
|
|
1000
|
+
"meta",
|
|
1001
|
+
"link",
|
|
1002
|
+
"script",
|
|
1003
|
+
"style",
|
|
1004
|
+
"div",
|
|
1005
|
+
"span",
|
|
1006
|
+
"p",
|
|
1007
|
+
"a",
|
|
1008
|
+
"img",
|
|
1009
|
+
"ul",
|
|
1010
|
+
"ol",
|
|
1011
|
+
"li",
|
|
1012
|
+
"table",
|
|
1013
|
+
"tr",
|
|
1014
|
+
"td",
|
|
1015
|
+
"th",
|
|
1016
|
+
"form",
|
|
1017
|
+
"input",
|
|
1018
|
+
"button",
|
|
1019
|
+
"label",
|
|
1020
|
+
"select",
|
|
1021
|
+
"option",
|
|
1022
|
+
"textarea",
|
|
1023
|
+
"header",
|
|
1024
|
+
"footer",
|
|
1025
|
+
"nav",
|
|
1026
|
+
"main",
|
|
1027
|
+
"section",
|
|
1028
|
+
"article",
|
|
1029
|
+
"aside",
|
|
1030
|
+
"canvas",
|
|
1031
|
+
"svg",
|
|
1032
|
+
"template",
|
|
1033
|
+
"slot"
|
|
1034
|
+
]);
|
|
935
1035
|
KEYWORD_SETS["javascript"] = KEYWORD_SETS["js"];
|
|
936
1036
|
KEYWORD_SETS["typescript"] = KEYWORD_SETS["ts"];
|
|
937
1037
|
KEYWORD_SETS["python"] = KEYWORD_SETS["py"];
|
|
938
1038
|
KEYWORD_SETS["rs"] = KEYWORD_SETS["rust"];
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1039
|
+
KEYWORD_SETS["jsx"] = KEYWORD_SETS["js"];
|
|
1040
|
+
KEYWORD_SETS["mjs"] = KEYWORD_SETS["js"];
|
|
1041
|
+
KEYWORD_SETS["cjs"] = KEYWORD_SETS["js"];
|
|
1042
|
+
KEYWORD_SETS["tsx"] = KEYWORD_SETS["ts"];
|
|
1043
|
+
KEYWORD_SETS["mts"] = KEYWORD_SETS["ts"];
|
|
1044
|
+
KEYWORD_SETS["cts"] = KEYWORD_SETS["ts"];
|
|
1045
|
+
KEYWORD_SETS["sh"] = KEYWORD_SETS["bash"];
|
|
1046
|
+
KEYWORD_SETS["zsh"] = KEYWORD_SETS["bash"];
|
|
1047
|
+
KEYWORD_SETS["shell"] = KEYWORD_SETS["bash"];
|
|
1048
|
+
KEYWORD_SETS["console"] = KEYWORD_SETS["bash"];
|
|
1049
|
+
KEYWORD_SETS["jsonc"] = KEYWORD_SETS["json"];
|
|
1050
|
+
KEYWORD_SETS["json5"] = KEYWORD_SETS["json"];
|
|
1051
|
+
KEYWORD_SETS["scss"] = KEYWORD_SETS["css"];
|
|
1052
|
+
KEYWORD_SETS["sass"] = KEYWORD_SETS["css"];
|
|
1053
|
+
KEYWORD_SETS["less"] = KEYWORD_SETS["css"];
|
|
1054
|
+
KEYWORD_SETS["vue"] = KEYWORD_SETS["html"];
|
|
1055
|
+
KEYWORD_SETS["svelte"] = KEYWORD_SETS["html"];
|
|
1056
|
+
KEYWORD_SETS["xml"] = KEYWORD_SETS["html"];
|
|
1057
|
+
KEYWORD_SETS["svg"] = KEYWORD_SETS["html"];
|
|
1058
|
+
var C_LIKE = {
|
|
1059
|
+
lineComments: ["//"],
|
|
1060
|
+
quotes: ['"', "'", "`"],
|
|
1061
|
+
numbers: true,
|
|
1062
|
+
blockComments: [["/*", "*/"]],
|
|
1063
|
+
// A JS/TS template literal spans lines. Listed here as well as in `quotes`:
|
|
1064
|
+
// `quotes` handles the common single-line case, and this carries the rest.
|
|
1065
|
+
multilineStrings: ["`"]
|
|
1066
|
+
};
|
|
1067
|
+
var HASH_COMMENT = {
|
|
1068
|
+
lineComments: ["#"],
|
|
1069
|
+
quotes: ['"', "'"],
|
|
1070
|
+
numbers: true
|
|
1071
|
+
};
|
|
1072
|
+
var LANGUAGE_SYNTAX = {
|
|
1073
|
+
js: C_LIKE,
|
|
1074
|
+
ts: C_LIKE,
|
|
1075
|
+
// A Python docstring is the language's block comment in practice, and it is
|
|
1076
|
+
// lexically a string, so it is carried as one rather than invented as a third
|
|
1077
|
+
// kind. Triple delimiters are listed before the single ones so the longest
|
|
1078
|
+
// match wins.
|
|
1079
|
+
py: { ...HASH_COMMENT, multilineStrings: ['"""', "'''"] },
|
|
1080
|
+
// Rust has `//` line comments AND `'` lifetimes. The unterminated-quote
|
|
1081
|
+
// fallback already keeps a lifetime from swallowing the line, so `'` stays
|
|
1082
|
+
// listed: `'a'` is a valid char literal and should color as a string.
|
|
1083
|
+
rust: C_LIKE,
|
|
1084
|
+
bash: HASH_COMMENT,
|
|
1085
|
+
// JSON has no comments and no single-quoted strings. JSONC does have `//`,
|
|
1086
|
+
// and is aliased separately below rather than sharing this entry.
|
|
1087
|
+
json: { lineComments: [], quotes: ['"'], numbers: true },
|
|
1088
|
+
// CSS has only block comments — which now span lines, so this entry claims
|
|
1089
|
+
// them. Numbers are everywhere in CSS and coloring them is most of the visible
|
|
1090
|
+
// benefit.
|
|
1091
|
+
css: {
|
|
1092
|
+
lineComments: [],
|
|
1093
|
+
quotes: ['"', "'"],
|
|
1094
|
+
numbers: true,
|
|
1095
|
+
blockComments: [["/*", "*/"]]
|
|
1096
|
+
},
|
|
1097
|
+
// Markup: no line comments, and numbers inside attribute values are noise
|
|
1098
|
+
// rather than signal. An SGML comment spans lines like any other block form.
|
|
1099
|
+
html: {
|
|
1100
|
+
lineComments: [],
|
|
1101
|
+
quotes: ['"', "'"],
|
|
1102
|
+
numbers: false,
|
|
1103
|
+
blockComments: [["<!--", "-->"]]
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
LANGUAGE_SYNTAX["javascript"] = LANGUAGE_SYNTAX["js"];
|
|
1107
|
+
LANGUAGE_SYNTAX["typescript"] = LANGUAGE_SYNTAX["ts"];
|
|
1108
|
+
LANGUAGE_SYNTAX["python"] = LANGUAGE_SYNTAX["py"];
|
|
1109
|
+
LANGUAGE_SYNTAX["rs"] = LANGUAGE_SYNTAX["rust"];
|
|
1110
|
+
LANGUAGE_SYNTAX["jsx"] = LANGUAGE_SYNTAX["js"];
|
|
1111
|
+
LANGUAGE_SYNTAX["mjs"] = LANGUAGE_SYNTAX["js"];
|
|
1112
|
+
LANGUAGE_SYNTAX["cjs"] = LANGUAGE_SYNTAX["js"];
|
|
1113
|
+
LANGUAGE_SYNTAX["tsx"] = LANGUAGE_SYNTAX["ts"];
|
|
1114
|
+
LANGUAGE_SYNTAX["mts"] = LANGUAGE_SYNTAX["ts"];
|
|
1115
|
+
LANGUAGE_SYNTAX["cts"] = LANGUAGE_SYNTAX["ts"];
|
|
1116
|
+
LANGUAGE_SYNTAX["sh"] = LANGUAGE_SYNTAX["bash"];
|
|
1117
|
+
LANGUAGE_SYNTAX["zsh"] = LANGUAGE_SYNTAX["bash"];
|
|
1118
|
+
LANGUAGE_SYNTAX["shell"] = LANGUAGE_SYNTAX["bash"];
|
|
1119
|
+
LANGUAGE_SYNTAX["console"] = LANGUAGE_SYNTAX["bash"];
|
|
1120
|
+
LANGUAGE_SYNTAX["yaml"] = HASH_COMMENT;
|
|
1121
|
+
LANGUAGE_SYNTAX["yml"] = HASH_COMMENT;
|
|
1122
|
+
LANGUAGE_SYNTAX["toml"] = HASH_COMMENT;
|
|
1123
|
+
LANGUAGE_SYNTAX["ini"] = HASH_COMMENT;
|
|
1124
|
+
LANGUAGE_SYNTAX["dockerfile"] = HASH_COMMENT;
|
|
1125
|
+
LANGUAGE_SYNTAX["makefile"] = HASH_COMMENT;
|
|
1126
|
+
LANGUAGE_SYNTAX["make"] = HASH_COMMENT;
|
|
1127
|
+
LANGUAGE_SYNTAX["jsonc"] = { lineComments: ["//"], quotes: ['"'], numbers: true };
|
|
1128
|
+
LANGUAGE_SYNTAX["json5"] = { lineComments: ["//"], quotes: ['"', "'"], numbers: true };
|
|
1129
|
+
LANGUAGE_SYNTAX["scss"] = C_LIKE;
|
|
1130
|
+
LANGUAGE_SYNTAX["sass"] = C_LIKE;
|
|
1131
|
+
LANGUAGE_SYNTAX["less"] = C_LIKE;
|
|
1132
|
+
LANGUAGE_SYNTAX["glsl"] = C_LIKE;
|
|
1133
|
+
LANGUAGE_SYNTAX["c"] = C_LIKE;
|
|
1134
|
+
LANGUAGE_SYNTAX["cpp"] = C_LIKE;
|
|
1135
|
+
LANGUAGE_SYNTAX["go"] = C_LIKE;
|
|
1136
|
+
LANGUAGE_SYNTAX["java"] = C_LIKE;
|
|
1137
|
+
LANGUAGE_SYNTAX["kotlin"] = C_LIKE;
|
|
1138
|
+
LANGUAGE_SYNTAX["swift"] = C_LIKE;
|
|
1139
|
+
LANGUAGE_SYNTAX["vue"] = LANGUAGE_SYNTAX["html"];
|
|
1140
|
+
LANGUAGE_SYNTAX["svelte"] = LANGUAGE_SYNTAX["html"];
|
|
1141
|
+
LANGUAGE_SYNTAX["xml"] = LANGUAGE_SYNTAX["html"];
|
|
1142
|
+
LANGUAGE_SYNTAX["svg"] = LANGUAGE_SYNTAX["html"];
|
|
1143
|
+
function highlightedLanguages() {
|
|
1144
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(LANGUAGE_SYNTAX), ...Object.keys(KEYWORD_SETS)])].sort();
|
|
1145
|
+
}
|
|
1146
|
+
function highlightLine(line, lang, theme, carry = null) {
|
|
1147
|
+
const key = lang.trim().toLowerCase().split(/[\s:,{]/)[0] ?? "";
|
|
1148
|
+
const keywords = KEYWORD_SETS[key];
|
|
1149
|
+
const syntax = LANGUAGE_SYNTAX[key];
|
|
1150
|
+
if (!keywords && !syntax) {
|
|
1151
|
+
return { segments: [{ text: line, color: theme.codeColor }], carry: null };
|
|
943
1152
|
}
|
|
944
1153
|
const segments = [];
|
|
945
1154
|
const KEYWORD_COLOR = theme.syntaxKeywordColor;
|
|
@@ -954,19 +1163,63 @@ function highlightLine(line, lang, theme) {
|
|
|
954
1163
|
buf = "";
|
|
955
1164
|
}
|
|
956
1165
|
};
|
|
1166
|
+
const lexical = syntax ?? C_LIKE;
|
|
1167
|
+
const findClose = (from, close, isString) => {
|
|
1168
|
+
let j = from;
|
|
1169
|
+
while (j < line.length) {
|
|
1170
|
+
if (isString && line[j] === "\\") {
|
|
1171
|
+
j += 2;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (line.startsWith(close, j)) return j + close.length;
|
|
1175
|
+
j++;
|
|
1176
|
+
}
|
|
1177
|
+
return -1;
|
|
1178
|
+
};
|
|
1179
|
+
if (carry) {
|
|
1180
|
+
const color = carry.kind === "comment" ? COMMENT_COLOR : STRING_COLOR;
|
|
1181
|
+
const end = findClose(0, carry.close, carry.kind === "string");
|
|
1182
|
+
if (end === -1) {
|
|
1183
|
+
if (line.length > 0) segments.push({ text: line, color });
|
|
1184
|
+
return { segments, carry };
|
|
1185
|
+
}
|
|
1186
|
+
segments.push({ text: line.slice(0, end), color });
|
|
1187
|
+
i = end;
|
|
1188
|
+
}
|
|
957
1189
|
while (i < line.length) {
|
|
958
1190
|
const ch = line[i];
|
|
959
|
-
|
|
1191
|
+
const block = lexical.blockComments?.find(([open]) => line.startsWith(open, i));
|
|
1192
|
+
if (block) {
|
|
1193
|
+
const [open, close] = block;
|
|
960
1194
|
flush(theme.codeColor);
|
|
961
|
-
|
|
962
|
-
|
|
1195
|
+
const end = findClose(i + open.length, close, false);
|
|
1196
|
+
if (end === -1) {
|
|
1197
|
+
segments.push({ text: line.slice(i), color: COMMENT_COLOR });
|
|
1198
|
+
return { segments, carry: { kind: "comment", close } };
|
|
1199
|
+
}
|
|
1200
|
+
segments.push({ text: line.slice(i, end), color: COMMENT_COLOR });
|
|
1201
|
+
i = end;
|
|
1202
|
+
continue;
|
|
963
1203
|
}
|
|
964
|
-
|
|
1204
|
+
const comment = lexical.lineComments.find((prefix) => line.startsWith(prefix, i));
|
|
1205
|
+
if (comment !== void 0) {
|
|
965
1206
|
flush(theme.codeColor);
|
|
966
1207
|
segments.push({ text: line.slice(i), color: COMMENT_COLOR });
|
|
967
|
-
return segments;
|
|
1208
|
+
return { segments, carry: null };
|
|
968
1209
|
}
|
|
969
|
-
|
|
1210
|
+
const multi = lexical.multilineStrings?.find((delim) => line.startsWith(delim, i));
|
|
1211
|
+
if (multi !== void 0) {
|
|
1212
|
+
flush(theme.codeColor);
|
|
1213
|
+
const end = findClose(i + multi.length, multi, true);
|
|
1214
|
+
if (end === -1) {
|
|
1215
|
+
segments.push({ text: line.slice(i), color: STRING_COLOR });
|
|
1216
|
+
return { segments, carry: { kind: "string", close: multi } };
|
|
1217
|
+
}
|
|
1218
|
+
segments.push({ text: line.slice(i, end), color: STRING_COLOR });
|
|
1219
|
+
i = end;
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (lexical.quotes.includes(ch)) {
|
|
970
1223
|
const quote = ch;
|
|
971
1224
|
let j = i + 1;
|
|
972
1225
|
let closed = false;
|
|
@@ -991,7 +1244,7 @@ function highlightLine(line, lang, theme) {
|
|
|
991
1244
|
i++;
|
|
992
1245
|
continue;
|
|
993
1246
|
}
|
|
994
|
-
if (/\d/.test(ch) && (i === 0 || /[\s(,=+\-*/<>[\]{}:;]/.test(line[i - 1]))) {
|
|
1247
|
+
if (lexical.numbers && /\d/.test(ch) && (i === 0 || /[\s(,=+\-*/<>[\]{}:;]/.test(line[i - 1]))) {
|
|
995
1248
|
flush(theme.codeColor);
|
|
996
1249
|
let j = i;
|
|
997
1250
|
while (j < line.length && /[\d._xXa-fA-F]/.test(line[j])) j++;
|
|
@@ -1006,7 +1259,9 @@ function highlightLine(line, lang, theme) {
|
|
|
1006
1259
|
const word = line.slice(i, j);
|
|
1007
1260
|
segments.push({
|
|
1008
1261
|
text: word,
|
|
1009
|
-
|
|
1262
|
+
// A language may have lexical syntax but no keywords (plain YAML, TOML,
|
|
1263
|
+
// a Dockerfile). Those still get comments, strings and numbers.
|
|
1264
|
+
color: keywords?.has(word) ? KEYWORD_COLOR : theme.codeColor
|
|
1010
1265
|
});
|
|
1011
1266
|
i = j;
|
|
1012
1267
|
continue;
|
|
@@ -1015,10 +1270,18 @@ function highlightLine(line, lang, theme) {
|
|
|
1015
1270
|
i++;
|
|
1016
1271
|
}
|
|
1017
1272
|
flush(theme.codeColor);
|
|
1018
|
-
return segments;
|
|
1273
|
+
return { segments, carry: null };
|
|
1019
1274
|
}
|
|
1020
1275
|
var CodeBlock = class extends UIComponent {
|
|
1021
1276
|
lines;
|
|
1277
|
+
/**
|
|
1278
|
+
* Lexical state ENTERING each line, index-aligned with {@link lines}.
|
|
1279
|
+
*
|
|
1280
|
+
* Entering rather than leaving, so a streamed append can resume tokenizing at
|
|
1281
|
+
* the prefix-reuse boundary by reading one entry instead of re-scanning the
|
|
1282
|
+
* document for an unclosed block comment.
|
|
1283
|
+
*/
|
|
1284
|
+
lineCarry = [];
|
|
1022
1285
|
grid = null;
|
|
1023
1286
|
/** Raw (unhighlighted) lines of the last build, for prefix reuse in buildLines. */
|
|
1024
1287
|
rawLines = null;
|
|
@@ -1026,6 +1289,19 @@ var CodeBlock = class extends UIComponent {
|
|
|
1026
1289
|
source;
|
|
1027
1290
|
/** Bumped by {@link buildLines} and {@link setSelectable}; read by `Scene`. */
|
|
1028
1291
|
contentEpoch = 0;
|
|
1292
|
+
/**
|
|
1293
|
+
* Horizontal scroll offset in local px, always in `[0, maxScrollX]`.
|
|
1294
|
+
*
|
|
1295
|
+
* Code does not wrap, so a line wider than the box would otherwise have an
|
|
1296
|
+
* unreachable tail. This offset is subtracted from BOTH the painted cell x and
|
|
1297
|
+
* the projected line x in the same frame — never one without the other, or the
|
|
1298
|
+
* DOM selection carriers detach from the glyphs they are supposed to cover
|
|
1299
|
+
* (the defect class `5cf7119` and `ee1de6f` fixed on the vertical axis).
|
|
1300
|
+
*/
|
|
1301
|
+
scrollXValue = 0;
|
|
1302
|
+
/** Memoized widest prepared line, keyed by the grid identity it came from. */
|
|
1303
|
+
contentWidthGrid = null;
|
|
1304
|
+
contentWidthValue = 0;
|
|
1029
1305
|
lang;
|
|
1030
1306
|
theme;
|
|
1031
1307
|
/**
|
|
@@ -1037,6 +1313,10 @@ var CodeBlock = class extends UIComponent {
|
|
|
1037
1313
|
pad;
|
|
1038
1314
|
codeFont;
|
|
1039
1315
|
selectable;
|
|
1316
|
+
/** Whether the language header band is drawn. See {@link CodeBlockOptions.showLanguage}. */
|
|
1317
|
+
showLanguage;
|
|
1318
|
+
/** Font of the header label, resolved once from the theme. */
|
|
1319
|
+
langFont;
|
|
1040
1320
|
/**
|
|
1041
1321
|
* @param theme Any subset of {@link MarkdownTheme}, or the name of a built-in
|
|
1042
1322
|
* preset (see {@link MarkdownThemePresetName}). Accepting a partial theme
|
|
@@ -1048,7 +1328,7 @@ var CodeBlock = class extends UIComponent {
|
|
|
1048
1328
|
* be constructed directly with a preset name without going through
|
|
1049
1329
|
* `Markdown`.
|
|
1050
1330
|
*/
|
|
1051
|
-
constructor(code, lang, maxWidth, theme, selectable = true) {
|
|
1331
|
+
constructor(code, lang, maxWidth, theme, selectable = true, options = {}) {
|
|
1052
1332
|
super();
|
|
1053
1333
|
const resolved = resolvePresetTheme(theme);
|
|
1054
1334
|
this.source = code;
|
|
@@ -1058,15 +1338,129 @@ var CodeBlock = class extends UIComponent {
|
|
|
1058
1338
|
this.pad = resolved.codePadding;
|
|
1059
1339
|
this.codeFont = `${resolved.codeFontSize}px ${resolved.codeFont}`;
|
|
1060
1340
|
this.selectable = selectable;
|
|
1341
|
+
this.langFont = `${resolved.codeLangFontSize}px ${resolved.codeFont}`;
|
|
1342
|
+
this.showLanguage = options.showLanguage === true && this.languageLabel() !== "";
|
|
1061
1343
|
this.lines = [];
|
|
1062
1344
|
this.width = maxWidth;
|
|
1063
1345
|
this.buildLines(code);
|
|
1346
|
+
this.on("wheel", (e) => {
|
|
1347
|
+
const max = this.maxScrollX;
|
|
1348
|
+
if (max <= 0) return;
|
|
1349
|
+
if (e.ctrlKey === true) return;
|
|
1350
|
+
const deltaMode = e.deltaMode ?? 0;
|
|
1351
|
+
let deltaX = e.deltaX ?? 0;
|
|
1352
|
+
let deltaY = e.deltaY ?? 0;
|
|
1353
|
+
if (deltaMode === 1) {
|
|
1354
|
+
deltaX *= 16;
|
|
1355
|
+
deltaY *= 16;
|
|
1356
|
+
} else if (deltaMode === 2) {
|
|
1357
|
+
deltaX *= this.width;
|
|
1358
|
+
deltaY *= this.height;
|
|
1359
|
+
}
|
|
1360
|
+
const horizontal = e.shiftKey === true ? deltaY || deltaX : deltaX;
|
|
1361
|
+
if (horizontal === 0) return;
|
|
1362
|
+
const before = this.scrollX;
|
|
1363
|
+
this.setScrollX(before + horizontal);
|
|
1364
|
+
if (this.scrollX !== before) e.nativeEvent?.preventDefault?.();
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* The language name shown in the header, or `''` when there is nothing to show.
|
|
1369
|
+
*
|
|
1370
|
+
* Normalized exactly as the highlighter normalizes its lookup key, so the label
|
|
1371
|
+
* and the colouring can never disagree about which language this is: a fence
|
|
1372
|
+
* may be written ` ```Bash ` or carry attributes (` ```ts title="a.ts" `), and
|
|
1373
|
+
* the label has to be the language, not the raw info string.
|
|
1374
|
+
*
|
|
1375
|
+
* Lowercased for the same reason `streamdown` lowercases its own
|
|
1376
|
+
* (`lib/code-block/header.tsx:15`): the fence's capitalization is incidental,
|
|
1377
|
+
* and a document mixing ` ```JS ` with ` ```js ` should not render two
|
|
1378
|
+
* different-looking labels for one language.
|
|
1379
|
+
*/
|
|
1380
|
+
languageLabel() {
|
|
1381
|
+
return this.lang.trim().toLowerCase().split(/[\s:,{]/)[0] ?? "";
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Height in px of the header band, or `0` when it is off.
|
|
1385
|
+
*
|
|
1386
|
+
* The label sits in a band of its own rather than floating over the code,
|
|
1387
|
+
* because a translucent overlay above real glyphs is unreadable at small sizes
|
|
1388
|
+
* and would fight the horizontal scroll: the code slides under it, so any text
|
|
1389
|
+
* drawn on top would collide with a different token every frame.
|
|
1390
|
+
*/
|
|
1391
|
+
headerHeight() {
|
|
1392
|
+
if (!this.showLanguage) return 0;
|
|
1393
|
+
return this.theme.codeLangFontSize + Math.round(this.pad * 0.75);
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Local y of the first line of code.
|
|
1397
|
+
*
|
|
1398
|
+
* Everything that positions a row — the painter, the projection, the grid's
|
|
1399
|
+
* own origin — goes through this, so the header offset cannot be applied to
|
|
1400
|
+
* one and forgotten on another. That class of mismatch is exactly what
|
|
1401
|
+
* detaches selection carriers from the glyphs they cover.
|
|
1402
|
+
*/
|
|
1403
|
+
contentTop() {
|
|
1404
|
+
return this.headerHeight() + this.pad;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Current horizontal scroll offset in local px, clamped to what the content
|
|
1408
|
+
* currently allows.
|
|
1409
|
+
*
|
|
1410
|
+
* Clamped on READ, not only on write, because `setWidth()` may shrink the box
|
|
1411
|
+
* after a scroll and is contractually forbidden from rebuilding anything. Both
|
|
1412
|
+
* the painter and the projection read through here, which is what keeps the
|
|
1413
|
+
* glyphs and the selection carriers on the same offset within a frame.
|
|
1414
|
+
*/
|
|
1415
|
+
get scrollX() {
|
|
1416
|
+
return Math.min(this.scrollXValue, this.maxScrollX);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Widest line's overflow past the padded box, i.e. the maximum useful
|
|
1420
|
+
* {@link scrollX}. `0` when every line already fits.
|
|
1421
|
+
*/
|
|
1422
|
+
get maxScrollX() {
|
|
1423
|
+
return Math.max(0, this.contentWidth() - (this.width - this.pad * 2));
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Widest prepared line, memoized against the grid that produced it.
|
|
1427
|
+
*
|
|
1428
|
+
* Read by {@link scrollX}, which both `render()` and `getContentProjection()`
|
|
1429
|
+
* call every synced frame, so an O(lines) scan here would be an O(document) cost
|
|
1430
|
+
* per frame on a long block — the exact shape the per-line projection window
|
|
1431
|
+
* exists to avoid. The grid is rebuilt only when the content changes, so the
|
|
1432
|
+
* cache key is identity of the grid object.
|
|
1433
|
+
*/
|
|
1434
|
+
contentWidth() {
|
|
1435
|
+
const grid = this.ensureGrid();
|
|
1436
|
+
if (this.contentWidthGrid === grid) return this.contentWidthValue;
|
|
1437
|
+
let widest = 0;
|
|
1438
|
+
for (const line of grid.lines) {
|
|
1439
|
+
if (line.width > widest) widest = line.width;
|
|
1440
|
+
}
|
|
1441
|
+
this.contentWidthGrid = grid;
|
|
1442
|
+
this.contentWidthValue = widest;
|
|
1443
|
+
return widest;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Scroll horizontally to `x`, clamped to `[0, maxScrollX]`.
|
|
1447
|
+
*
|
|
1448
|
+
* @returns `this` for chaining.
|
|
1449
|
+
*/
|
|
1450
|
+
setScrollX(x) {
|
|
1451
|
+
const next = Math.max(0, Math.min(this.maxScrollX, x));
|
|
1452
|
+
if (next === this.scrollXValue) return this;
|
|
1453
|
+
this.scrollXValue = next;
|
|
1454
|
+
this.contentEpoch++;
|
|
1455
|
+
this.scene?.markDirty();
|
|
1456
|
+
return this;
|
|
1064
1457
|
}
|
|
1065
1458
|
/** Re-parse code content (e.g. for live editing). */
|
|
1066
1459
|
setCode(code, lang) {
|
|
1067
1460
|
if (lang !== void 0) this.lang = lang;
|
|
1068
1461
|
this.source = code;
|
|
1069
1462
|
this.buildLines(code);
|
|
1463
|
+
this.scrollXValue = Math.min(this.scrollXValue, this.maxScrollX);
|
|
1070
1464
|
this.scene?.markDirty();
|
|
1071
1465
|
return this;
|
|
1072
1466
|
}
|
|
@@ -1086,9 +1480,13 @@ var CodeBlock = class extends UIComponent {
|
|
|
1086
1480
|
* Deliberately does **not** rebuild the grid or the highlight, because code does
|
|
1087
1481
|
* not reflow: lines are placed on a fixed monospace grid at `col × cellWidth` and
|
|
1088
1482
|
* a long line overflows rather than wrapping, so `height` is a function of line
|
|
1089
|
-
* *count* alone. The width
|
|
1090
|
-
* change the glyph geometry — the source, the language, the font —
|
|
1091
|
-
* {@link setCode} and invalidates the grid there.
|
|
1483
|
+
* *count* alone. The width sizes the rounded background and the clip. Anything
|
|
1484
|
+
* that would change the glyph geometry — the source, the language, the font —
|
|
1485
|
+
* goes through {@link setCode} and invalidates the grid there.
|
|
1486
|
+
*
|
|
1487
|
+
* A narrower box can leave {@link scrollX} past the new end of travel. That is
|
|
1488
|
+
* resolved by clamping on read rather than by adjusting anything here, so this
|
|
1489
|
+
* method keeps costing nothing.
|
|
1092
1490
|
*
|
|
1093
1491
|
* @returns `this` for chaining.
|
|
1094
1492
|
*/
|
|
@@ -1102,16 +1500,21 @@ var CodeBlock = class extends UIComponent {
|
|
|
1102
1500
|
getContentProjection(hint) {
|
|
1103
1501
|
if (!this.source) return null;
|
|
1104
1502
|
const grid = this.ensureGrid();
|
|
1503
|
+
const scrollX = this.scrollX;
|
|
1105
1504
|
const rows = [];
|
|
1106
1505
|
rows.length = grid.lines.length;
|
|
1107
1506
|
for (let row = 0; row < grid.lines.length; row++) {
|
|
1108
1507
|
const line = grid.lines[row];
|
|
1109
|
-
const y = this.
|
|
1508
|
+
const y = this.contentTop() + row * this.lineH;
|
|
1110
1509
|
if (!contentLineInHint(hint, y, this.lineH)) continue;
|
|
1111
1510
|
rows[row] = {
|
|
1112
1511
|
text: this.source.slice(line.sourceStart, line.sourceEnd),
|
|
1113
1512
|
separatorAfter: this.source.slice(line.sourceEnd, line.nextSourceStart) || void 0,
|
|
1114
|
-
|
|
1513
|
+
// The SAME offset `render()` subtracts, read through the same clamping
|
|
1514
|
+
// accessor. Cell carriers are `position: relative` inside this `absolute`
|
|
1515
|
+
// line box, so shifting the line's x translates every cell of the line
|
|
1516
|
+
// rigidly and selection stays over the glyphs.
|
|
1517
|
+
x: this.pad - scrollX,
|
|
1115
1518
|
y,
|
|
1116
1519
|
baseline: this.lineH * 0.75,
|
|
1117
1520
|
font: this.codeFont,
|
|
@@ -1131,6 +1534,12 @@ var CodeBlock = class extends UIComponent {
|
|
|
1131
1534
|
// render() draws cell-by-cell (no ligatures can form); the DOM copy
|
|
1132
1535
|
// must not ligate either or Firefox selection geometry drifts.
|
|
1133
1536
|
ligatures: "none",
|
|
1537
|
+
// `render()` clips the glyph pass to this box, so the DOM copy must too.
|
|
1538
|
+
// A line wider than the box otherwise projects carriers past the entity,
|
|
1539
|
+
// and the browser paints their selection highlight over whatever is drawn
|
|
1540
|
+
// beside the block — measured 1580px of carrier against a 1566px viewport
|
|
1541
|
+
// on a real page, the highlight running through the prose to its right.
|
|
1542
|
+
clipToBounds: true,
|
|
1134
1543
|
grid
|
|
1135
1544
|
};
|
|
1136
1545
|
}
|
|
@@ -1145,6 +1554,11 @@ var CodeBlock = class extends UIComponent {
|
|
|
1145
1554
|
*
|
|
1146
1555
|
* The last previously-seen line is deliberately NOT reused: a chunk usually
|
|
1147
1556
|
* lands mid-line, so that line's text (and therefore its tokenization) changes.
|
|
1557
|
+
*
|
|
1558
|
+
* Prefix reuse survives multi-line constructs because {@link lineCarry} records
|
|
1559
|
+
* the state ENTERING each line, so resuming at the reuse boundary needs no
|
|
1560
|
+
* rescan: a carried state is a pure function of the preceding text, and that
|
|
1561
|
+
* text is byte-identical over the reused prefix by construction.
|
|
1148
1562
|
*/
|
|
1149
1563
|
buildLines(code) {
|
|
1150
1564
|
this.contentEpoch++;
|
|
@@ -1155,18 +1569,20 @@ var CodeBlock = class extends UIComponent {
|
|
|
1155
1569
|
const limit = Math.min(previous.length - 1, rawLines.length);
|
|
1156
1570
|
while (reusable < limit && previous[reusable] === rawLines[reusable]) reusable++;
|
|
1157
1571
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
this.
|
|
1164
|
-
|
|
1165
|
-
|
|
1572
|
+
const lines = reusable > 0 ? this.lines.slice(0, reusable) : [];
|
|
1573
|
+
const carries = reusable > 0 ? this.lineCarry.slice(0, reusable) : [];
|
|
1574
|
+
let carry = reusable > 0 ? this.lineCarry[reusable] ?? null : null;
|
|
1575
|
+
for (let i = reusable; i < rawLines.length; i++) {
|
|
1576
|
+
carries.push(carry);
|
|
1577
|
+
const result = highlightLine(rawLines[i], this.lang, this.theme, carry);
|
|
1578
|
+
lines.push(result.segments);
|
|
1579
|
+
carry = result.carry;
|
|
1166
1580
|
}
|
|
1581
|
+
this.lines = lines;
|
|
1582
|
+
this.lineCarry = carries;
|
|
1167
1583
|
this.rawLines = rawLines;
|
|
1168
1584
|
this.grid = null;
|
|
1169
|
-
this.height = this.
|
|
1585
|
+
this.height = this.contentTop() + this.pad + rawLines.length * this.lineH;
|
|
1170
1586
|
}
|
|
1171
1587
|
ensureGrid() {
|
|
1172
1588
|
const cellWidth = this.cellWidth || Math.max(1, measureText("M", this.codeFont));
|
|
@@ -1180,7 +1596,15 @@ var CodeBlock = class extends UIComponent {
|
|
|
1180
1596
|
}
|
|
1181
1597
|
return this.grid;
|
|
1182
1598
|
}
|
|
1183
|
-
/**
|
|
1599
|
+
/**
|
|
1600
|
+
* Not hit-testable, and deliberately still not `interactive`, even though the
|
|
1601
|
+
* block now consumes wheel events to scroll.
|
|
1602
|
+
*
|
|
1603
|
+
* The wheel arrives from the content-projection div rather than from canvas
|
|
1604
|
+
* hit-testing, so no a11y shadow node is needed. Creating one would place a
|
|
1605
|
+
* `pointer-events: auto` element above the transparent text mirror and swallow
|
|
1606
|
+
* the mousedown that starts a native drag-selection.
|
|
1607
|
+
*/
|
|
1184
1608
|
isPointInside() {
|
|
1185
1609
|
return false;
|
|
1186
1610
|
}
|
|
@@ -1192,8 +1616,25 @@ var CodeBlock = class extends UIComponent {
|
|
|
1192
1616
|
const atlas = codeGlyphAtlas(r);
|
|
1193
1617
|
const atlasSource = atlas?.source ?? null;
|
|
1194
1618
|
const blit = atlas ? r.drawImageRect : void 0;
|
|
1619
|
+
const header = this.headerHeight();
|
|
1620
|
+
if (header > 0) {
|
|
1621
|
+
r.fillText(
|
|
1622
|
+
this.languageLabel(),
|
|
1623
|
+
this.pad,
|
|
1624
|
+
// Vertically centred in the band by its own cap height rather than by
|
|
1625
|
+
// font size: `fillText` takes a baseline, so centring the em box would
|
|
1626
|
+
// sit the visible letterforms low. 0.7 of the label size below the band's
|
|
1627
|
+
// centre line is where a lowercase-plus-cap run reads as centred.
|
|
1628
|
+
(header + this.theme.codeLangFontSize * 0.7) / 2,
|
|
1629
|
+
this.langFont,
|
|
1630
|
+
this.theme.codeLangColor
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
r.save();
|
|
1634
|
+
r.clip(0, header, this.width, this.height - header);
|
|
1635
|
+
const scrollX = this.scrollX;
|
|
1195
1636
|
for (let row = 0; row < grid.lines.length; row++) {
|
|
1196
|
-
const yBaseline = this.
|
|
1637
|
+
const yBaseline = this.contentTop() + row * this.lineH + this.lineH * 0.75;
|
|
1197
1638
|
const segments = this.lines[row];
|
|
1198
1639
|
let segmentIndex = 0;
|
|
1199
1640
|
let segmentEnd = segments[0]?.text.length ?? 0;
|
|
@@ -1207,7 +1648,8 @@ var CodeBlock = class extends UIComponent {
|
|
|
1207
1648
|
const sourceText = this.source.slice(cell.sourceStart, cell.sourceEnd);
|
|
1208
1649
|
if (cell.advance <= 0 || sourceText === " " || sourceText === " ") continue;
|
|
1209
1650
|
const color = segments[segmentIndex]?.color ?? this.theme.codeColor;
|
|
1210
|
-
const x = this.pad + cell.x;
|
|
1651
|
+
const x = this.pad + cell.x - scrollX;
|
|
1652
|
+
if (x + cell.advance < 0 || x > this.width) continue;
|
|
1211
1653
|
if (blit && atlas) {
|
|
1212
1654
|
const slot = atlas.get(this.codeFont, color, cell.glyph);
|
|
1213
1655
|
const src = atlasSource ?? atlas.source;
|
|
@@ -1230,6 +1672,7 @@ var CodeBlock = class extends UIComponent {
|
|
|
1230
1672
|
r.fillText(cell.glyph, x, yBaseline, this.codeFont, color);
|
|
1231
1673
|
}
|
|
1232
1674
|
}
|
|
1675
|
+
r.restore();
|
|
1233
1676
|
}
|
|
1234
1677
|
};
|
|
1235
1678
|
var codeAtlases = /* @__PURE__ */ new Map();
|
|
@@ -2481,6 +2924,20 @@ function defaultSaveFile(filename, content, mimeType) {
|
|
|
2481
2924
|
doc.body.removeChild(anchor);
|
|
2482
2925
|
URL.revokeObjectURL(url);
|
|
2483
2926
|
}
|
|
2927
|
+
function resolveBlockAffordanceConfig(config = {}) {
|
|
2928
|
+
return {
|
|
2929
|
+
copy: config.copy ?? true,
|
|
2930
|
+
download: config.download ?? true,
|
|
2931
|
+
labels: {
|
|
2932
|
+
copyCode: config.labels?.copyCode ?? "Copy code",
|
|
2933
|
+
downloadCode: config.labels?.downloadCode ?? "Download code",
|
|
2934
|
+
copyTable: config.labels?.copyTable ?? "Copy table",
|
|
2935
|
+
downloadTable: config.labels?.downloadTable ?? "Download table",
|
|
2936
|
+
copied: config.labels?.copied ?? "Copied",
|
|
2937
|
+
saved: config.labels?.saved ?? "Saved"
|
|
2938
|
+
}
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2484
2941
|
var BlockAffordanceButton = class _BlockAffordanceButton extends Button {
|
|
2485
2942
|
constructor(label, successLabel, act, opts = {}) {
|
|
2486
2943
|
super(label, { ...opts, onClick: () => this.run() });
|
|
@@ -2735,6 +3192,12 @@ marked.use({
|
|
|
2735
3192
|
}
|
|
2736
3193
|
]
|
|
2737
3194
|
});
|
|
3195
|
+
var MISWIRED_LAYOUT_HOOK_NAMES = [
|
|
3196
|
+
"onHeightChanged",
|
|
3197
|
+
"onHeightChange",
|
|
3198
|
+
"onLayoutUpdate",
|
|
3199
|
+
"onResize"
|
|
3200
|
+
];
|
|
2738
3201
|
var markdownWorker = null;
|
|
2739
3202
|
var workerIdCounter = 0;
|
|
2740
3203
|
var workerInstanceCounter = 0;
|
|
@@ -2796,20 +3259,62 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
2796
3259
|
* affordance.
|
|
2797
3260
|
*/
|
|
2798
3261
|
blockAffordances;
|
|
3262
|
+
/**
|
|
3263
|
+
* Which controls a block carries and what they are called, with defaults
|
|
3264
|
+
* applied.
|
|
3265
|
+
*
|
|
3266
|
+
* Resolved once in the constructor rather than per block: the defaults are
|
|
3267
|
+
* fixed, and re-deriving them for every code fence in a long document would
|
|
3268
|
+
* repeat the same six `??` fallbacks for no benefit.
|
|
3269
|
+
*/
|
|
3270
|
+
affordanceConfig;
|
|
3271
|
+
/**
|
|
3272
|
+
* Whether code blocks show their language in a header band.
|
|
3273
|
+
*
|
|
3274
|
+
* Read when a block entity is built, exactly like {@link blockAffordances}, so
|
|
3275
|
+
* it affects blocks rendered from here on rather than retroactively.
|
|
3276
|
+
*/
|
|
3277
|
+
showCodeLanguage;
|
|
2799
3278
|
/** Clipboard writer used by the copy controls. */
|
|
2800
3279
|
writeClipboard;
|
|
2801
3280
|
/** File saver used by the download controls. */
|
|
2802
3281
|
saveFile;
|
|
2803
3282
|
activeBlockMetrics = null;
|
|
2804
3283
|
/**
|
|
2805
|
-
* Called after
|
|
3284
|
+
* Called after this entity's own `width`/`height` changed because the document
|
|
3285
|
+
* was re-laid-out. **This is the hook to wire up when a host has to move or
|
|
3286
|
+
* resize anything positioned below the document.**
|
|
3287
|
+
*
|
|
3288
|
+
* Fires from three paths, all of which republish `width`/`height` from
|
|
3289
|
+
* `content`:
|
|
3290
|
+
*
|
|
3291
|
+
* - a streamed append (`updateTokens`),
|
|
3292
|
+
* - a width change ({@link setMaxWidth}),
|
|
3293
|
+
* - a paragraph image whose decoded bitmap corrected the guessed aspect ratio
|
|
3294
|
+
* (`reflowAfterImageResize`) — the guess is a flat 16:10, so this one fires
|
|
3295
|
+
* on essentially every document containing an image, and a host that misses
|
|
3296
|
+
* it leaves every block below the image overlapping it.
|
|
3297
|
+
*
|
|
3298
|
+
* It does **not** fire from `setContent()`, which replaces the whole document
|
|
3299
|
+
* and is a call the host already made, so this is a re-layout signal rather
|
|
3300
|
+
* than a complete size signal.
|
|
3301
|
+
*
|
|
3302
|
+
* A `VirtualList` needs none of this to track a streaming row: it re-reads
|
|
3303
|
+
* `height` on every mounted row each frame, so it sees this entity grow without
|
|
3304
|
+
* being told. Prefer that where it applies. Reach for this callback when the
|
|
3305
|
+
* host owns absolute positions of its own — a page that stacks navigation, a
|
|
3306
|
+
* footer and a scroll height under the document has to recompute them here.
|
|
2806
3307
|
*
|
|
2807
|
-
*
|
|
2808
|
-
*
|
|
2809
|
-
*
|
|
2810
|
-
* path only, **not** from `setContent()`, so it is not a complete size signal.
|
|
3308
|
+
* There is no `onHeightChanged`. That name has been assigned by real callers
|
|
3309
|
+
* through an `as unknown as` cast, which compiles, silences the type error and
|
|
3310
|
+
* then never fires; if a layout callback appears dead, check the name first.
|
|
2811
3311
|
*/
|
|
2812
3312
|
onLayoutUpdated;
|
|
3313
|
+
/**
|
|
3314
|
+
* Latch for the miswired-hook warning, so a streaming document warns once
|
|
3315
|
+
* rather than on every chunk.
|
|
3316
|
+
*/
|
|
3317
|
+
hasWarnedLayoutHookName = false;
|
|
2813
3318
|
/**
|
|
2814
3319
|
* The document's BODY text — everything after any front matter block.
|
|
2815
3320
|
*
|
|
@@ -3052,6 +3557,8 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
3052
3557
|
this.selectable = opts.selectable ?? true;
|
|
3053
3558
|
this._userTiming = opts.userTiming ?? false;
|
|
3054
3559
|
this.blockAffordances = opts.blockAffordances ?? false;
|
|
3560
|
+
this.affordanceConfig = resolveBlockAffordanceConfig(opts.affordances);
|
|
3561
|
+
this.showCodeLanguage = opts.showCodeLanguage ?? false;
|
|
3055
3562
|
this.writeClipboard = opts.writeClipboard ?? defaultWriteClipboard;
|
|
3056
3563
|
this.saveFile = opts.saveFile ?? defaultSaveFile;
|
|
3057
3564
|
this.content = new Stack({
|
|
@@ -3260,7 +3767,7 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
3260
3767
|
this.content.layout();
|
|
3261
3768
|
this.width = this.content.width;
|
|
3262
3769
|
this.height = this.content.height;
|
|
3263
|
-
this.
|
|
3770
|
+
this.notifyLayoutUpdated();
|
|
3264
3771
|
this.scene?.markDirty();
|
|
3265
3772
|
return this;
|
|
3266
3773
|
}
|
|
@@ -4013,40 +4520,60 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
4013
4520
|
const controls = make();
|
|
4014
4521
|
return controls.length > 0 ? new BlockWithAffordances(block, controls) : block;
|
|
4015
4522
|
}
|
|
4016
|
-
/** Copy and download controls for one fenced code block. */
|
|
4523
|
+
/** Copy and download controls for one fenced code block, per {@link affordanceConfig}. */
|
|
4017
4524
|
codeBlockAffordances(source, lang) {
|
|
4018
4525
|
const opts = this.affordanceButtonOptions();
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4526
|
+
const { copy, download, labels } = this.affordanceConfig;
|
|
4527
|
+
const controls = [];
|
|
4528
|
+
if (copy) {
|
|
4529
|
+
controls.push(
|
|
4530
|
+
new BlockAffordanceButton(
|
|
4531
|
+
labels.copyCode,
|
|
4532
|
+
labels.copied,
|
|
4533
|
+
() => this.writeClipboard(source),
|
|
4534
|
+
opts
|
|
4535
|
+
)
|
|
4536
|
+
);
|
|
4537
|
+
}
|
|
4538
|
+
if (download) {
|
|
4539
|
+
controls.push(
|
|
4540
|
+
new BlockAffordanceButton(
|
|
4541
|
+
labels.downloadCode,
|
|
4542
|
+
labels.saved,
|
|
4543
|
+
() => this.saveFile(`code.${extensionForLanguage(lang)}`, source, mimeForLanguage(lang)),
|
|
4544
|
+
opts
|
|
4545
|
+
)
|
|
4546
|
+
);
|
|
4547
|
+
}
|
|
4548
|
+
return controls;
|
|
4549
|
+
}
|
|
4550
|
+
/** Copy (as Markdown) and download (as CSV) controls for one table, per {@link affordanceConfig}. */
|
|
4030
4551
|
tableAffordances(tblToken) {
|
|
4552
|
+
const { copy, download, labels } = this.affordanceConfig;
|
|
4031
4553
|
const content = tableContentOf(tblToken);
|
|
4032
4554
|
const opts = this.affordanceButtonOptions();
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
)
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4555
|
+
const controls = [];
|
|
4556
|
+
if (copy) {
|
|
4557
|
+
controls.push(
|
|
4558
|
+
new BlockAffordanceButton(
|
|
4559
|
+
labels.copyTable,
|
|
4560
|
+
labels.copied,
|
|
4561
|
+
() => this.writeClipboard(tableToMarkdown(content)),
|
|
4562
|
+
opts
|
|
4563
|
+
)
|
|
4564
|
+
);
|
|
4565
|
+
}
|
|
4566
|
+
if (download) {
|
|
4567
|
+
controls.push(
|
|
4568
|
+
new BlockAffordanceButton(
|
|
4569
|
+
labels.downloadTable,
|
|
4570
|
+
labels.saved,
|
|
4571
|
+
() => this.saveFile("table.csv", tableToCsv(content), "text/csv;charset=utf-8"),
|
|
4572
|
+
opts
|
|
4573
|
+
)
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4576
|
+
return controls;
|
|
4050
4577
|
}
|
|
4051
4578
|
/**
|
|
4052
4579
|
* Button styling for the affordances, derived from the document theme.
|
|
@@ -4130,7 +4657,40 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
4130
4657
|
}
|
|
4131
4658
|
this.width = this.content.width;
|
|
4132
4659
|
this.height = this.content.height;
|
|
4133
|
-
this.
|
|
4660
|
+
this.notifyLayoutUpdated();
|
|
4661
|
+
}
|
|
4662
|
+
/**
|
|
4663
|
+
* Publish a completed re-layout to the host.
|
|
4664
|
+
*
|
|
4665
|
+
* Every path that republishes `width`/`height` from `content` ends here rather
|
|
4666
|
+
* than calling {@link onLayoutUpdated} directly, so the misuse check below is
|
|
4667
|
+
* reached however the re-layout was triggered.
|
|
4668
|
+
*
|
|
4669
|
+
* The check exists because the failure it catches is silent and was found in
|
|
4670
|
+
* production, not in review. A host wired its reflow to `onHeightChanged` — a
|
|
4671
|
+
* name this class has never had — through an `as unknown as` cast. The cast
|
|
4672
|
+
* satisfied the compiler, the callback never fired, and every post containing
|
|
4673
|
+
* an image stayed laid out against the guessed 16:10 aspect ratio with a stale
|
|
4674
|
+
* document scroll height. Nothing in the type system, the tests or the console
|
|
4675
|
+
* said anything. A property that is *only ever assigned* has no read site to
|
|
4676
|
+
* fail, so the one place that can notice is the moment we would have called it.
|
|
4677
|
+
*/
|
|
4678
|
+
notifyLayoutUpdated() {
|
|
4679
|
+
if (this.onLayoutUpdated) {
|
|
4680
|
+
this.onLayoutUpdated();
|
|
4681
|
+
return;
|
|
4682
|
+
}
|
|
4683
|
+
if (!this.hasWarnedLayoutHookName) {
|
|
4684
|
+
const wrongName = MISWIRED_LAYOUT_HOOK_NAMES.find(
|
|
4685
|
+
(name) => typeof this[name] === "function"
|
|
4686
|
+
);
|
|
4687
|
+
if (wrongName) {
|
|
4688
|
+
this.hasWarnedLayoutHookName = true;
|
|
4689
|
+
console.warn(
|
|
4690
|
+
`[VectoJS] Markdown.${wrongName} is not a VectoJS callback and will never fire. The document just re-laid-out and nothing was notified. Assign \`onLayoutUpdated\` instead \u2014 anything positioned below the document needs it, and a paragraph image corrects its guessed aspect ratio on decode, so a missed signal leaves following blocks overlapping the image.`
|
|
4691
|
+
);
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4134
4694
|
}
|
|
4135
4695
|
/**
|
|
4136
4696
|
* Re-derive one `MarkdownContainer`'s cached box from its children.
|
|
@@ -4941,9 +5501,7 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
4941
5501
|
this.width = this.content.width;
|
|
4942
5502
|
this.height = this.content.height;
|
|
4943
5503
|
this.scene?.markDirty();
|
|
4944
|
-
|
|
4945
|
-
this.onLayoutUpdated();
|
|
4946
|
-
}
|
|
5504
|
+
this.notifyLayoutUpdated();
|
|
4947
5505
|
}
|
|
4948
5506
|
/**
|
|
4949
5507
|
* Render one nested block with a temporary width/margin context while
|
|
@@ -5159,7 +5717,9 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
5159
5717
|
}
|
|
5160
5718
|
}
|
|
5161
5719
|
return this.withBlockAffordances(
|
|
5162
|
-
new CodeBlock(codeToken.text, lang, availableWidth, t, this.selectable
|
|
5720
|
+
new CodeBlock(codeToken.text, lang, availableWidth, t, this.selectable, {
|
|
5721
|
+
showLanguage: this.showCodeLanguage
|
|
5722
|
+
}),
|
|
5163
5723
|
() => this.codeBlockAffordances(codeToken.text, lang)
|
|
5164
5724
|
);
|
|
5165
5725
|
}
|
|
@@ -5390,6 +5950,7 @@ export {
|
|
|
5390
5950
|
extensionForLanguage,
|
|
5391
5951
|
footnoteMarker,
|
|
5392
5952
|
hasFencedBlockRenderer,
|
|
5953
|
+
highlightedLanguages,
|
|
5393
5954
|
isFencedBlockRendererReady,
|
|
5394
5955
|
isMathJaxReady,
|
|
5395
5956
|
isPresetName,
|
|
@@ -5398,6 +5959,7 @@ export {
|
|
|
5398
5959
|
preloadMathJax,
|
|
5399
5960
|
registerFencedBlockRenderer,
|
|
5400
5961
|
renderFencedBlock,
|
|
5962
|
+
resolveBlockAffordanceConfig,
|
|
5401
5963
|
resolvePresetTheme,
|
|
5402
5964
|
scanFrontMatter,
|
|
5403
5965
|
tableContentOf,
|