quikchat 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/build-manifest.json +73 -80
- package/dist/quikchat-md-full.cjs.js +536 -255
- package/dist/quikchat-md-full.cjs.js.map +1 -1
- package/dist/quikchat-md-full.cjs.min.js +3 -3
- package/dist/quikchat-md-full.cjs.min.js.map +1 -1
- package/dist/quikchat-md-full.esm.js +536 -255
- package/dist/quikchat-md-full.esm.js.map +1 -1
- package/dist/quikchat-md-full.esm.min.js +3 -3
- package/dist/quikchat-md-full.esm.min.js.map +1 -1
- package/dist/quikchat-md-full.umd.js +536 -255
- package/dist/quikchat-md-full.umd.js.map +1 -1
- package/dist/quikchat-md-full.umd.min.js +3 -3
- package/dist/quikchat-md-full.umd.min.js.map +1 -1
- package/dist/quikchat-md.cjs.js +520 -245
- package/dist/quikchat-md.cjs.js.map +1 -1
- package/dist/quikchat-md.cjs.min.js +3 -3
- package/dist/quikchat-md.cjs.min.js.map +1 -1
- package/dist/quikchat-md.esm.js +520 -245
- package/dist/quikchat-md.esm.js.map +1 -1
- package/dist/quikchat-md.esm.min.js +3 -3
- package/dist/quikchat-md.esm.min.js.map +1 -1
- package/dist/quikchat-md.umd.js +520 -245
- package/dist/quikchat-md.umd.js.map +1 -1
- package/dist/quikchat-md.umd.min.js +3 -3
- package/dist/quikchat-md.umd.min.js.map +1 -1
- package/dist/quikchat.cjs.js +2 -2
- package/dist/quikchat.cjs.js.map +1 -1
- package/dist/quikchat.cjs.min.js +1 -1
- package/dist/quikchat.cjs.min.js.map +1 -1
- package/dist/quikchat.css +351 -120
- package/dist/quikchat.esm.js +2 -2
- package/dist/quikchat.esm.js.map +1 -1
- package/dist/quikchat.esm.min.js +1 -1
- package/dist/quikchat.esm.min.js.map +1 -1
- package/dist/quikchat.min.css +1 -1
- package/dist/quikchat.umd.js +2 -2
- package/dist/quikchat.umd.js.map +1 -1
- package/dist/quikchat.umd.min.js +1 -1
- package/dist/quikchat.umd.min.js.map +1 -1
- package/package.json +6 -5
|
@@ -594,7 +594,7 @@ var quikchat = /*#__PURE__*/function () {
|
|
|
594
594
|
value: function _autoGrowTextarea() {
|
|
595
595
|
var el = this._textEntry;
|
|
596
596
|
el.style.height = 'auto';
|
|
597
|
-
var maxHeight =
|
|
597
|
+
var maxHeight = 120;
|
|
598
598
|
el.style.height = Math.min(el.scrollHeight, maxHeight) + 'px';
|
|
599
599
|
el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
|
600
600
|
}
|
|
@@ -1062,7 +1062,7 @@ var quikchat = /*#__PURE__*/function () {
|
|
|
1062
1062
|
key: "version",
|
|
1063
1063
|
value: function version() {
|
|
1064
1064
|
return {
|
|
1065
|
-
"version": "1.2.
|
|
1065
|
+
"version": "1.2.6",
|
|
1066
1066
|
"license": "BSD-2",
|
|
1067
1067
|
"url": "https://github/deftio/quikchat"
|
|
1068
1068
|
};
|
|
@@ -1122,35 +1122,140 @@ var quikchat = /*#__PURE__*/function () {
|
|
|
1122
1122
|
|
|
1123
1123
|
/**
|
|
1124
1124
|
* quikdown_bd - Bidirectional Markdown Parser
|
|
1125
|
-
* @version 1.
|
|
1125
|
+
* @version 1.2.10
|
|
1126
1126
|
* @license BSD-2-Clause
|
|
1127
1127
|
* @copyright DeftIO 2025
|
|
1128
1128
|
*/
|
|
1129
1129
|
/**
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1130
|
+
* quikdown_classify — Shared line-classification utilities
|
|
1131
|
+
* ═════════════════════════════════════════════════════════
|
|
1132
|
+
*
|
|
1133
|
+
* Pure functions for classifying markdown lines. Used by both the main
|
|
1134
|
+
* parser (quikdown.js) and the editor (quikdown_edit.js) so the logic
|
|
1135
|
+
* lives in one place.
|
|
1136
|
+
*
|
|
1137
|
+
* All functions operate on a **trimmed** line (caller must trim).
|
|
1138
|
+
* None use regexes with nested quantifiers — every check is either a
|
|
1139
|
+
* simple regex or a linear scan, so there is zero ReDoS risk.
|
|
1140
1140
|
*/
|
|
1141
1141
|
|
|
1142
|
-
// Version will be injected at build time
|
|
1143
|
-
const quikdownVersion = '1.1.1';
|
|
1144
1142
|
|
|
1145
|
-
|
|
1143
|
+
/**
|
|
1144
|
+
* Dash-only HR check — exact parity with the main parser's original
|
|
1145
|
+
* regex `/^---+\s*$/`. Only matches lines of three or more dashes
|
|
1146
|
+
* with optional trailing whitespace (no interspersed spaces).
|
|
1147
|
+
*
|
|
1148
|
+
* @param {string} trimmed The line, already trimmed
|
|
1149
|
+
* @returns {boolean}
|
|
1150
|
+
*/
|
|
1151
|
+
function isDashHRLine(trimmed) {
|
|
1152
|
+
if (trimmed.length < 3) return false;
|
|
1153
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
1154
|
+
const ch = trimmed[i];
|
|
1155
|
+
if (ch === '-') continue;
|
|
1156
|
+
// Allow trailing whitespace only
|
|
1157
|
+
if (ch === ' ' || ch === '\t') {
|
|
1158
|
+
for (let j = i + 1; j < trimmed.length; j++) {
|
|
1159
|
+
if (trimmed[j] !== ' ' && trimmed[j] !== '\t') return false;
|
|
1160
|
+
}
|
|
1161
|
+
return i >= 3; // at least 3 dashes before whitespace
|
|
1162
|
+
}
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
return true; // all dashes
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* quikdown — A compact, scanner-based markdown parser
|
|
1170
|
+
* ════════════════════════════════════════════════════
|
|
1171
|
+
*
|
|
1172
|
+
* Architecture overview (v1.2.8 — lexer rewrite)
|
|
1173
|
+
* ───────────────────────────────────────────────
|
|
1174
|
+
* Prior to v1.2.8, quikdown used a multi-pass regex pipeline: each block
|
|
1175
|
+
* type (headings, blockquotes, HR, lists, tables) and each inline format
|
|
1176
|
+
* (bold, italic, links, …) was handled by its own global regex applied
|
|
1177
|
+
* sequentially to the full document string. That worked but made the code
|
|
1178
|
+
* hard to extend and debug — a new construct meant adding another regex
|
|
1179
|
+
* pass, and ordering bugs between passes were subtle.
|
|
1180
|
+
*
|
|
1181
|
+
* Starting in v1.2.8 the parser uses a **line-scanning** approach for
|
|
1182
|
+
* block detection and a **per-block inline pass** for formatting:
|
|
1183
|
+
*
|
|
1184
|
+
* ┌─────────────────────────────────────────────────────────┐
|
|
1185
|
+
* │ Phase 1 — Code Extraction │
|
|
1186
|
+
* │ Scan for fenced code blocks (``` / ~~~) and inline │
|
|
1187
|
+
* │ code spans (`…`). Replace with §CB§ / §IC§ place- │
|
|
1188
|
+
* │ holders so code content is never touched by later │
|
|
1189
|
+
* │ phases. │
|
|
1190
|
+
* ├─────────────────────────────────────────────────────────┤
|
|
1191
|
+
* │ Phase 2 — HTML Escaping │
|
|
1192
|
+
* │ Escape &, <, >, ", ' in the remaining text to prevent │
|
|
1193
|
+
* │ XSS. (Skipped when allow_unsafe_html is true.) │
|
|
1194
|
+
* ├─────────────────────────────────────────────────────────┤
|
|
1195
|
+
* │ Phase 3 — Block Scanning │
|
|
1196
|
+
* │ Walk the text **line by line**. At each line, the │
|
|
1197
|
+
* │ scanner checks (in order): │
|
|
1198
|
+
* │ • table rows (|) │
|
|
1199
|
+
* │ • headings (#) │
|
|
1200
|
+
* │ • HR (---) │
|
|
1201
|
+
* │ • blockquotes (>) │
|
|
1202
|
+
* │ • list items (-, *, +, 1.) │
|
|
1203
|
+
* │ • code-block placeholder (§CB…§) │
|
|
1204
|
+
* │ • paragraph text (everything else) │
|
|
1205
|
+
* │ │
|
|
1206
|
+
* │ Block text is run through the **inline formatter** │
|
|
1207
|
+
* │ which handles bold, italic, strikethrough, links, │
|
|
1208
|
+
* │ images, and autolinks. │
|
|
1209
|
+
* │ │
|
|
1210
|
+
* │ Paragraphs are wrapped in <p> tags. Lazy linefeeds │
|
|
1211
|
+
* │ (single \n → <br>) are handled here too. │
|
|
1212
|
+
* ├─────────────────────────────────────────────────────────┤
|
|
1213
|
+
* │ Phase 4 — Code Restoration │
|
|
1214
|
+
* │ Replace §CB§ / §IC§ placeholders with rendered <pre> │
|
|
1215
|
+
* │ / <code> HTML, applying the fence_plugin if present. │
|
|
1216
|
+
* └─────────────────────────────────────────────────────────┘
|
|
1217
|
+
*
|
|
1218
|
+
* Why this design?
|
|
1219
|
+
* • Single pass over lines for block identification — no re-scanning.
|
|
1220
|
+
* • Each block type is a clearly separated branch, easy to add new ones.
|
|
1221
|
+
* • Inline formatting is confined to block text — can't accidentally
|
|
1222
|
+
* match across block boundaries or inside HTML tags.
|
|
1223
|
+
* • Code extraction still uses a simple regex (it's one pattern, not a
|
|
1224
|
+
* chain) because the §-placeholder approach is proven and simple.
|
|
1225
|
+
*
|
|
1226
|
+
* @param {string} markdown The markdown source text
|
|
1227
|
+
* @param {Object} options Configuration (see below)
|
|
1228
|
+
* @returns {string} Rendered HTML
|
|
1229
|
+
*/
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1233
|
+
// Constants
|
|
1234
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1235
|
+
|
|
1236
|
+
/** Build-time version stamp (injected by tools/updateVersion) */
|
|
1237
|
+
const quikdownVersion = '1.2.10';
|
|
1238
|
+
|
|
1239
|
+
/** CSS class prefix used for all generated elements */
|
|
1146
1240
|
const CLASS_PREFIX = 'quikdown-';
|
|
1147
|
-
const PLACEHOLDER_CB = '§CB';
|
|
1148
|
-
const PLACEHOLDER_IC = '§IC';
|
|
1149
1241
|
|
|
1150
|
-
|
|
1242
|
+
/** Placeholder sigils — chosen to be extremely unlikely in real text */
|
|
1243
|
+
const PLACEHOLDER_CB = '§CB'; // fenced code blocks
|
|
1244
|
+
const PLACEHOLDER_IC = '§IC'; // inline code spans
|
|
1245
|
+
|
|
1246
|
+
/** HTML entity escape map */
|
|
1151
1247
|
const ESC_MAP = {'&':'&','<':'<','>':'>','"':'"',"'":'''};
|
|
1152
1248
|
|
|
1153
|
-
//
|
|
1249
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1250
|
+
// Style definitions
|
|
1251
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* Inline styles for every element quikdown can emit.
|
|
1255
|
+
* When `inline_styles: true` these are injected as style="…" attributes.
|
|
1256
|
+
* When `inline_styles: false` (default) we use class="quikdown-<tag>"
|
|
1257
|
+
* and these same values are emitted by `quikdown.emitStyles()`.
|
|
1258
|
+
*/
|
|
1154
1259
|
const QUIKDOWN_STYLES = {
|
|
1155
1260
|
h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',
|
|
1156
1261
|
h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',
|
|
@@ -1173,30 +1278,41 @@ const QUIKDOWN_STYLES = {
|
|
|
1173
1278
|
ul: 'margin:.5em 0;padding-left:2em',
|
|
1174
1279
|
ol: 'margin:.5em 0;padding-left:2em',
|
|
1175
1280
|
li: 'margin:.25em 0',
|
|
1176
|
-
// Task list specific styles
|
|
1177
1281
|
'task-item': 'list-style:none',
|
|
1178
1282
|
'task-checkbox': 'margin-right:.5em'
|
|
1179
1283
|
};
|
|
1180
1284
|
|
|
1181
|
-
//
|
|
1285
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1286
|
+
// Attribute factory
|
|
1287
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Creates a `getAttr(tag, additionalStyle?)` helper that returns
|
|
1291
|
+
* either a class="…" or style="…" attribute string depending on mode.
|
|
1292
|
+
*
|
|
1293
|
+
* @param {boolean} inline_styles True → emit style="…"; false → class="…"
|
|
1294
|
+
* @param {Object} styles The QUIKDOWN_STYLES map
|
|
1295
|
+
* @returns {Function}
|
|
1296
|
+
*/
|
|
1182
1297
|
function createGetAttr(inline_styles, styles) {
|
|
1183
1298
|
return function(tag, additionalStyle = '') {
|
|
1184
1299
|
if (inline_styles) {
|
|
1185
1300
|
let style = styles[tag];
|
|
1186
1301
|
if (!style && !additionalStyle) return '';
|
|
1187
|
-
|
|
1188
|
-
//
|
|
1302
|
+
|
|
1303
|
+
// When adding alignment that conflicts with the tag's default,
|
|
1304
|
+
// strip the default text-align first.
|
|
1189
1305
|
if (additionalStyle && additionalStyle.includes('text-align') && style && style.includes('text-align')) {
|
|
1190
1306
|
style = style.replace(/text-align:[^;]+;?/, '').trim();
|
|
1307
|
+
/* istanbul ignore next */
|
|
1191
1308
|
if (style && !style.endsWith(';')) style += ';';
|
|
1192
1309
|
}
|
|
1193
|
-
|
|
1310
|
+
|
|
1194
1311
|
/* istanbul ignore next - defensive: additionalStyle without style doesn't occur with current tags */
|
|
1195
1312
|
const fullStyle = additionalStyle ? (style ? `${style}${additionalStyle}` : additionalStyle) : style;
|
|
1196
1313
|
return ` style="${fullStyle}"`;
|
|
1197
1314
|
} else {
|
|
1198
1315
|
const classAttr = ` class="${CLASS_PREFIX}${tag}"`;
|
|
1199
|
-
// Apply inline styles for alignment even when using CSS classes
|
|
1200
1316
|
if (additionalStyle) {
|
|
1201
1317
|
return `${classAttr} style="${additionalStyle}"`;
|
|
1202
1318
|
}
|
|
@@ -1205,69 +1321,84 @@ function createGetAttr(inline_styles, styles) {
|
|
|
1205
1321
|
};
|
|
1206
1322
|
}
|
|
1207
1323
|
|
|
1324
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1325
|
+
// Main parser function
|
|
1326
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1327
|
+
|
|
1208
1328
|
function quikdown(markdown, options = {}) {
|
|
1329
|
+
// ── Guard: only process non-empty strings ──
|
|
1209
1330
|
if (!markdown || typeof markdown !== 'string') {
|
|
1210
1331
|
return '';
|
|
1211
1332
|
}
|
|
1212
|
-
|
|
1213
|
-
const { fence_plugin, inline_styles = false, bidirectional = false, lazy_linefeeds = false } = options;
|
|
1214
|
-
const styles = QUIKDOWN_STYLES; // Use module-level styles
|
|
1215
|
-
const getAttr = createGetAttr(inline_styles, styles); // Create getAttr once
|
|
1216
1333
|
|
|
1217
|
-
//
|
|
1334
|
+
// ── Unpack options ──
|
|
1335
|
+
const { fence_plugin, inline_styles = false, bidirectional = false, lazy_linefeeds = false, allow_unsafe_html = false } = options;
|
|
1336
|
+
const styles = QUIKDOWN_STYLES;
|
|
1337
|
+
const getAttr = createGetAttr(inline_styles, styles);
|
|
1338
|
+
|
|
1339
|
+
// ── Helpers (closed over options) ──
|
|
1340
|
+
|
|
1341
|
+
/** Escape the five HTML-special characters. */
|
|
1218
1342
|
function escapeHtml(text) {
|
|
1219
1343
|
return text.replace(/[&<>"']/g, m => ESC_MAP[m]);
|
|
1220
1344
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Bidirectional marker helper.
|
|
1348
|
+
* When bidirectional mode is on, returns ` data-qd="…"`.
|
|
1349
|
+
* The non-bidirectional branch is a trivial no-op arrow; it is
|
|
1350
|
+
* exercised in the core bundle but never in quikdown_bd.
|
|
1351
|
+
*/
|
|
1352
|
+
/* istanbul ignore next - trivial no-op fallback */
|
|
1223
1353
|
const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
|
|
1224
|
-
|
|
1225
|
-
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Sanitize a URL to block javascript:, vbscript:, and non-image data: URIs.
|
|
1357
|
+
* Returns '#' for blocked URLs.
|
|
1358
|
+
*/
|
|
1226
1359
|
function sanitizeUrl(url, allowUnsafe = false) {
|
|
1227
1360
|
/* istanbul ignore next - defensive programming, regex ensures url is never empty */
|
|
1228
1361
|
if (!url) return '';
|
|
1229
|
-
|
|
1230
|
-
// If unsafe URLs are explicitly allowed, return as-is
|
|
1231
1362
|
if (allowUnsafe) return url;
|
|
1232
|
-
|
|
1363
|
+
|
|
1233
1364
|
const trimmedUrl = url.trim();
|
|
1234
1365
|
const lowerUrl = trimmedUrl.toLowerCase();
|
|
1235
|
-
|
|
1236
|
-
// Block dangerous protocols
|
|
1237
1366
|
const dangerousProtocols = ['javascript:', 'vbscript:', 'data:'];
|
|
1238
|
-
|
|
1367
|
+
|
|
1239
1368
|
for (const protocol of dangerousProtocols) {
|
|
1240
1369
|
if (lowerUrl.startsWith(protocol)) {
|
|
1241
|
-
// Exception: Allow data:image/* for images
|
|
1242
1370
|
if (protocol === 'data:' && lowerUrl.startsWith('data:image/')) {
|
|
1243
1371
|
return trimmedUrl;
|
|
1244
1372
|
}
|
|
1245
|
-
// Return safe empty link for dangerous protocols
|
|
1246
1373
|
return '#';
|
|
1247
1374
|
}
|
|
1248
1375
|
}
|
|
1249
|
-
|
|
1250
1376
|
return trimmedUrl;
|
|
1251
1377
|
}
|
|
1252
1378
|
|
|
1253
|
-
//
|
|
1379
|
+
// ────────────────────────────────────────────────────────────────
|
|
1380
|
+
// Phase 1 — Code Extraction
|
|
1381
|
+
// ────────────────────────────────────────────────────────────────
|
|
1382
|
+
// Why extract code first? Fenced blocks and inline code spans can
|
|
1383
|
+
// contain markdown-like characters (*, _, #, |, etc.) that must NOT
|
|
1384
|
+
// be interpreted as formatting. By pulling them out and replacing
|
|
1385
|
+
// with unique placeholders, the rest of the pipeline never sees them.
|
|
1386
|
+
|
|
1254
1387
|
let html = markdown;
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
//
|
|
1261
|
-
//
|
|
1262
|
-
// Fence must be at start of line
|
|
1388
|
+
const codeBlocks = []; // Array of {lang, code, custom, fence, hasReverse}
|
|
1389
|
+
const inlineCodes = []; // Array of escaped-HTML strings
|
|
1390
|
+
|
|
1391
|
+
// ── Fenced code blocks ──
|
|
1392
|
+
// Matches paired fences: ``` with ``` and ~~~ with ~~~.
|
|
1393
|
+
// The fence must start at column 0 of a line (^ with /m flag).
|
|
1394
|
+
// Group 1 = fence marker, Group 2 = language hint, Group 3 = code body.
|
|
1263
1395
|
html = html.replace(/^(```|~~~)([^\n]*)\n([\s\S]*?)^\1$/gm, (match, fence, lang, code) => {
|
|
1264
1396
|
const placeholder = `${PLACEHOLDER_CB}${codeBlocks.length}§`;
|
|
1265
|
-
|
|
1266
|
-
// Trim the language specification
|
|
1267
1397
|
const langTrimmed = lang ? lang.trim() : '';
|
|
1268
|
-
|
|
1269
|
-
// If custom fence plugin is provided, use it (v1.1.0: object format required)
|
|
1398
|
+
|
|
1270
1399
|
if (fence_plugin && fence_plugin.render && typeof fence_plugin.render === 'function') {
|
|
1400
|
+
// Custom plugin — store raw code (un-escaped) so the plugin
|
|
1401
|
+
// receives the original source.
|
|
1271
1402
|
codeBlocks.push({
|
|
1272
1403
|
lang: langTrimmed,
|
|
1273
1404
|
code: code.trimEnd(),
|
|
@@ -1276,6 +1407,7 @@ function quikdown(markdown, options = {}) {
|
|
|
1276
1407
|
hasReverse: !!fence_plugin.reverse
|
|
1277
1408
|
});
|
|
1278
1409
|
} else {
|
|
1410
|
+
// Default — pre-escape the code for safe HTML output.
|
|
1279
1411
|
codeBlocks.push({
|
|
1280
1412
|
lang: langTrimmed,
|
|
1281
1413
|
code: escapeHtml(code.trimEnd()),
|
|
@@ -1285,66 +1417,97 @@ function quikdown(markdown, options = {}) {
|
|
|
1285
1417
|
}
|
|
1286
1418
|
return placeholder;
|
|
1287
1419
|
});
|
|
1288
|
-
|
|
1289
|
-
//
|
|
1420
|
+
|
|
1421
|
+
// ── Inline code spans ──
|
|
1422
|
+
// Matches a single backtick pair: `content`.
|
|
1423
|
+
// Content is captured and HTML-escaped immediately.
|
|
1290
1424
|
html = html.replace(/`([^`]+)`/g, (match, code) => {
|
|
1291
1425
|
const placeholder = `${PLACEHOLDER_IC}${inlineCodes.length}§`;
|
|
1292
1426
|
inlineCodes.push(escapeHtml(code));
|
|
1293
1427
|
return placeholder;
|
|
1294
1428
|
});
|
|
1295
|
-
|
|
1296
|
-
//
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
//
|
|
1300
|
-
|
|
1301
|
-
//
|
|
1429
|
+
|
|
1430
|
+
// ────────────────────────────────────────────────────────────────
|
|
1431
|
+
// Phase 2 — HTML Escaping
|
|
1432
|
+
// ────────────────────────────────────────────────────────────────
|
|
1433
|
+
// All remaining text (everything except code placeholders) is escaped
|
|
1434
|
+
// to prevent XSS. The `allow_unsafe_html` option skips this for
|
|
1435
|
+
// trusted pipelines that intentionally embed raw HTML.
|
|
1436
|
+
|
|
1437
|
+
if (!allow_unsafe_html) {
|
|
1438
|
+
html = escapeHtml(html);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// ────────────────────────────────────────────────────────────────
|
|
1442
|
+
// Phase 3 — Block Scanning + Inline Formatting + Paragraphs
|
|
1443
|
+
// ────────────────────────────────────────────────────────────────
|
|
1444
|
+
// This is the heart of the lexer rewrite. Instead of applying
|
|
1445
|
+
// 10+ global regex passes, we:
|
|
1446
|
+
// 1. Process tables (line walker — tables need multi-line lookahead)
|
|
1447
|
+
// 2. Scan remaining lines for headings, HR, blockquotes
|
|
1448
|
+
// 3. Process lists (line walker — lists need indent tracking)
|
|
1449
|
+
// 4. Apply inline formatting to all text content
|
|
1450
|
+
// 5. Wrap remaining text in <p> tags
|
|
1451
|
+
//
|
|
1452
|
+
// Steps 1 and 3 are line-walkers that process the full text in a
|
|
1453
|
+
// single pass each. Step 2 replaces global regex with a per-line
|
|
1454
|
+
// scanner. Steps 4-5 are applied to the result.
|
|
1455
|
+
//
|
|
1456
|
+
// Total: 3 structured passes instead of 10+ regex passes.
|
|
1457
|
+
|
|
1458
|
+
// ── Step 1: Tables ──
|
|
1459
|
+
// Tables need multi-line lookahead (header → separator → body rows)
|
|
1460
|
+
// so they're handled by a dedicated line-walker first.
|
|
1302
1461
|
html = processTable(html, getAttr);
|
|
1303
|
-
|
|
1304
|
-
//
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
//
|
|
1311
|
-
|
|
1312
|
-
// Merge consecutive blockquotes
|
|
1313
|
-
html = html.replace(/<\/blockquote>\n<blockquote>/g, '\n');
|
|
1314
|
-
|
|
1315
|
-
// Process horizontal rules (allow trailing spaces)
|
|
1316
|
-
html = html.replace(/^---+\s*$/gm, `<hr${getAttr('hr')}>`);
|
|
1317
|
-
|
|
1318
|
-
// Process lists
|
|
1462
|
+
|
|
1463
|
+
// ── Step 2: Headings, HR, Blockquotes ──
|
|
1464
|
+
// These are simple line-level constructs. We scan each line once
|
|
1465
|
+
// and replace matching lines with their HTML representation.
|
|
1466
|
+
html = scanLineBlocks(html, getAttr, dataQd);
|
|
1467
|
+
|
|
1468
|
+
// ── Step 3: Lists ──
|
|
1469
|
+
// Lists need indent-level tracking across lines, so they get their
|
|
1470
|
+
// own line-walker.
|
|
1319
1471
|
html = processLists(html, getAttr, inline_styles, bidirectional);
|
|
1320
|
-
|
|
1321
|
-
//
|
|
1322
|
-
|
|
1323
|
-
//
|
|
1472
|
+
|
|
1473
|
+
// ── Step 4: Inline formatting ──
|
|
1474
|
+
// Apply bold, italic, strikethrough, images, links, and autolinks
|
|
1475
|
+
// to all text content. This runs on the output of steps 1-3, so
|
|
1476
|
+
// it sees text inside headings, blockquotes, table cells, list
|
|
1477
|
+
// items, and paragraph text.
|
|
1478
|
+
|
|
1479
|
+
// Images (must come before links —  vs [text](url))
|
|
1324
1480
|
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
|
1325
1481
|
const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
|
|
1482
|
+
/* istanbul ignore next - bd-only branch */
|
|
1326
1483
|
const altAttr = bidirectional && alt ? ` data-qd-alt="${escapeHtml(alt)}"` : '';
|
|
1484
|
+
/* istanbul ignore next - bd-only branch */
|
|
1327
1485
|
const srcAttr = bidirectional ? ` data-qd-src="${escapeHtml(src)}"` : '';
|
|
1328
1486
|
return `<img${getAttr('img')} src="${sanitizedSrc}" alt="${alt}"${altAttr}${srcAttr}${dataQd('!')}>`;
|
|
1329
1487
|
});
|
|
1330
|
-
|
|
1331
|
-
// Links
|
|
1488
|
+
|
|
1489
|
+
// Links
|
|
1332
1490
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
|
|
1333
|
-
// Sanitize URL to prevent XSS
|
|
1334
1491
|
const sanitizedHref = sanitizeUrl(href, options.allow_unsafe_urls);
|
|
1335
1492
|
const isExternal = /^https?:\/\//i.test(sanitizedHref);
|
|
1336
1493
|
const rel = isExternal ? ' rel="noopener noreferrer"' : '';
|
|
1494
|
+
/* istanbul ignore next - bd-only branch */
|
|
1337
1495
|
const textAttr = bidirectional ? ` data-qd-text="${escapeHtml(text)}"` : '';
|
|
1338
1496
|
return `<a${getAttr('a')} href="${sanitizedHref}"${rel}${textAttr}${dataQd('[')}>${text}</a>`;
|
|
1339
1497
|
});
|
|
1340
|
-
|
|
1341
|
-
// Autolinks
|
|
1498
|
+
|
|
1499
|
+
// Autolinks — bare https?:// URLs become clickable <a> tags
|
|
1342
1500
|
html = html.replace(/(^|\s)(https?:\/\/[^\s<]+)/g, (match, prefix, url) => {
|
|
1343
1501
|
const sanitizedUrl = sanitizeUrl(url, options.allow_unsafe_urls);
|
|
1344
1502
|
return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
|
|
1345
1503
|
});
|
|
1346
|
-
|
|
1347
|
-
//
|
|
1504
|
+
|
|
1505
|
+
// Protect rendered tags so emphasis regexes don't see attribute
|
|
1506
|
+
// values — fixes #3 (underscores in URLs interpreted as emphasis).
|
|
1507
|
+
const savedTags = [];
|
|
1508
|
+
html = html.replace(/<[^>]+>/g, m => { savedTags.push(m); return `%%T${savedTags.length - 1}%%`; });
|
|
1509
|
+
|
|
1510
|
+
// Bold, italic, strikethrough
|
|
1348
1511
|
const inlinePatterns = [
|
|
1349
1512
|
[/\*\*(.+?)\*\*/g, 'strong', '**'],
|
|
1350
1513
|
[/__(.+?)__/g, 'strong', '__'],
|
|
@@ -1352,60 +1515,66 @@ function quikdown(markdown, options = {}) {
|
|
|
1352
1515
|
[/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em', '_'],
|
|
1353
1516
|
[/~~(.+?)~~/g, 'del', '~~']
|
|
1354
1517
|
];
|
|
1355
|
-
|
|
1356
1518
|
inlinePatterns.forEach(([pattern, tag, marker]) => {
|
|
1357
1519
|
html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);
|
|
1358
1520
|
});
|
|
1359
|
-
|
|
1360
|
-
//
|
|
1521
|
+
|
|
1522
|
+
// Restore protected tags
|
|
1523
|
+
html = html.replace(/%%T(\d+)%%/g, (_, i) => savedTags[i]);
|
|
1524
|
+
|
|
1525
|
+
// ── Step 5: Line breaks + paragraph wrapping ──
|
|
1361
1526
|
if (lazy_linefeeds) {
|
|
1362
|
-
// Lazy linefeeds: single
|
|
1527
|
+
// Lazy linefeeds mode: every single \n becomes <br> EXCEPT:
|
|
1528
|
+
// • Double newlines → paragraph break
|
|
1529
|
+
// • Newlines adjacent to block elements (h, blockquote, pre, hr, table, list)
|
|
1530
|
+
//
|
|
1531
|
+
// Strategy: protect block-adjacent newlines with §N§, convert
|
|
1532
|
+
// the rest, then restore.
|
|
1533
|
+
|
|
1363
1534
|
const blocks = [];
|
|
1364
1535
|
let bi = 0;
|
|
1365
|
-
|
|
1366
|
-
// Protect tables and lists
|
|
1536
|
+
|
|
1537
|
+
// Protect tables and lists from <br> injection
|
|
1367
1538
|
html = html.replace(/<(table|[uo]l)[^>]*>[\s\S]*?<\/\1>/g, m => {
|
|
1368
1539
|
blocks[bi] = m;
|
|
1369
1540
|
return `§B${bi++}§`;
|
|
1370
1541
|
});
|
|
1371
|
-
|
|
1372
|
-
// Handle paragraphs and block elements
|
|
1542
|
+
|
|
1373
1543
|
html = html.replace(/\n\n+/g, '§P§')
|
|
1374
|
-
// After block
|
|
1544
|
+
// After block-level closing tags
|
|
1375
1545
|
.replace(/(<\/(?:h[1-6]|blockquote|pre)>)\n/g, '$1§N§')
|
|
1376
1546
|
.replace(/(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)\n/g, '$1§N§')
|
|
1377
|
-
// Before block
|
|
1547
|
+
// Before block-level opening tags
|
|
1378
1548
|
.replace(/\n(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)/g, '§N§$1')
|
|
1379
1549
|
.replace(/\n(§B\d+§)/g, '§N§$1')
|
|
1380
1550
|
.replace(/(§B\d+§)\n/g, '$1§N§')
|
|
1381
|
-
// Convert
|
|
1551
|
+
// Convert surviving newlines to <br>
|
|
1382
1552
|
.replace(/\n/g, `<br${getAttr('br')}>`)
|
|
1383
1553
|
// Restore
|
|
1384
1554
|
.replace(/§N§/g, '\n')
|
|
1385
1555
|
.replace(/§P§/g, '</p><p>');
|
|
1386
|
-
|
|
1556
|
+
|
|
1387
1557
|
// Restore protected blocks
|
|
1388
1558
|
blocks.forEach((b, i) => html = html.replace(`§B${i}§`, b));
|
|
1389
|
-
|
|
1559
|
+
|
|
1390
1560
|
html = '<p>' + html + '</p>';
|
|
1391
1561
|
} else {
|
|
1392
|
-
// Standard: two spaces
|
|
1393
|
-
html = html.replace(/
|
|
1394
|
-
|
|
1395
|
-
// Paragraphs (double newlines)
|
|
1396
|
-
// Don't add </p> after block elements (they're not in paragraphs)
|
|
1562
|
+
// Standard mode: two trailing spaces → <br>, double newline → new paragraph
|
|
1563
|
+
html = html.replace(/ {2}$/gm, `<br${getAttr('br')}>`);
|
|
1564
|
+
|
|
1397
1565
|
html = html.replace(/\n\n+/g, (match, offset) => {
|
|
1398
|
-
// Check if we're after a block element closing tag
|
|
1399
1566
|
const before = html.substring(0, offset);
|
|
1400
1567
|
if (before.match(/<\/(h[1-6]|blockquote|ul|ol|table|pre|hr)>$/)) {
|
|
1401
|
-
return '<p>';
|
|
1568
|
+
return '<p>';
|
|
1402
1569
|
}
|
|
1403
|
-
return '</p><p>';
|
|
1570
|
+
return '</p><p>';
|
|
1404
1571
|
});
|
|
1405
1572
|
html = '<p>' + html + '</p>';
|
|
1406
1573
|
}
|
|
1407
|
-
|
|
1408
|
-
//
|
|
1574
|
+
|
|
1575
|
+
// ── Step 6: Cleanup ──
|
|
1576
|
+
// Remove <p> wrappers that accidentally enclose block elements.
|
|
1577
|
+
// This is simpler than trying to prevent them during wrapping.
|
|
1409
1578
|
const cleanupPatterns = [
|
|
1410
1579
|
[/<p><\/p>/g, ''],
|
|
1411
1580
|
[/<p>(<h[1-6][^>]*>)/g, '$1'],
|
|
@@ -1419,67 +1588,154 @@ function quikdown(markdown, options = {}) {
|
|
|
1419
1588
|
[/(<\/table>)<\/p>/g, '$1'],
|
|
1420
1589
|
[/<p>(<pre[^>]*>)/g, '$1'],
|
|
1421
1590
|
[/(<\/pre>)<\/p>/g, '$1'],
|
|
1422
|
-
[new RegExp(`<p>(${PLACEHOLDER_CB}\\d+§)
|
|
1591
|
+
[new RegExp(`<p>(${PLACEHOLDER_CB}\\d+§)</p>`, 'g'), '$1']
|
|
1423
1592
|
];
|
|
1424
|
-
|
|
1425
1593
|
cleanupPatterns.forEach(([pattern, replacement]) => {
|
|
1426
1594
|
html = html.replace(pattern, replacement);
|
|
1427
1595
|
});
|
|
1428
|
-
|
|
1429
|
-
//
|
|
1430
|
-
// When a paragraph follows a block element, ensure it has opening <p>
|
|
1596
|
+
|
|
1597
|
+
// When a block element is followed by a newline and then text, open a <p>.
|
|
1431
1598
|
html = html.replace(/(<\/(?:h[1-6]|blockquote|ul|ol|table|pre|hr)>)\n([^<])/g, '$1\n<p>$2');
|
|
1432
|
-
|
|
1433
|
-
//
|
|
1434
|
-
|
|
1435
|
-
//
|
|
1599
|
+
|
|
1600
|
+
// ────────────────────────────────────────────────────────────────
|
|
1601
|
+
// Phase 4 — Code Restoration
|
|
1602
|
+
// ────────────────────────────────────────────────────────────────
|
|
1603
|
+
// Replace placeholders with rendered HTML. For fenced blocks this
|
|
1604
|
+
// means wrapping in <pre><code>…</code></pre> (or calling the
|
|
1605
|
+
// fence_plugin). For inline code it means <code>…</code>.
|
|
1606
|
+
|
|
1436
1607
|
codeBlocks.forEach((block, i) => {
|
|
1437
1608
|
let replacement;
|
|
1438
|
-
|
|
1609
|
+
|
|
1439
1610
|
if (block.custom && fence_plugin && fence_plugin.render) {
|
|
1440
|
-
//
|
|
1611
|
+
// Delegate to the user-provided fence plugin.
|
|
1441
1612
|
replacement = fence_plugin.render(block.code, block.lang);
|
|
1442
|
-
|
|
1443
|
-
// If plugin returns undefined, fall back to default rendering
|
|
1613
|
+
|
|
1444
1614
|
if (replacement === undefined) {
|
|
1615
|
+
// Plugin declined — fall back to default rendering.
|
|
1445
1616
|
const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
|
|
1446
1617
|
const codeAttr = inline_styles ? getAttr('code') : langClass;
|
|
1618
|
+
/* istanbul ignore next - bd-only branch */
|
|
1447
1619
|
const langAttr = bidirectional && block.lang ? ` data-qd-lang="${escapeHtml(block.lang)}"` : '';
|
|
1620
|
+
/* istanbul ignore next - bd-only branch */
|
|
1448
1621
|
const fenceAttr = bidirectional ? ` data-qd-fence="${escapeHtml(block.fence)}"` : '';
|
|
1449
1622
|
replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${escapeHtml(block.code)}</code></pre>`;
|
|
1450
|
-
} else if (bidirectional) {
|
|
1451
|
-
//
|
|
1452
|
-
replacement = replacement.replace(/^<(\w+)/,
|
|
1623
|
+
} else /* istanbul ignore next - bd-only branch */ if (bidirectional) {
|
|
1624
|
+
// Plugin returned HTML — inject data attributes for roundtrip.
|
|
1625
|
+
replacement = replacement.replace(/^<(\w+)/,
|
|
1453
1626
|
`<$1 data-qd-fence="${escapeHtml(block.fence)}" data-qd-lang="${escapeHtml(block.lang)}" data-qd-source="${escapeHtml(block.code)}"`);
|
|
1454
1627
|
}
|
|
1455
1628
|
} else {
|
|
1456
|
-
// Default rendering
|
|
1629
|
+
// Default rendering — wrap in <pre><code>.
|
|
1457
1630
|
const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
|
|
1458
1631
|
const codeAttr = inline_styles ? getAttr('code') : langClass;
|
|
1632
|
+
/* istanbul ignore next - bd-only branch */
|
|
1459
1633
|
const langAttr = bidirectional && block.lang ? ` data-qd-lang="${escapeHtml(block.lang)}"` : '';
|
|
1634
|
+
/* istanbul ignore next - bd-only branch */
|
|
1460
1635
|
const fenceAttr = bidirectional ? ` data-qd-fence="${escapeHtml(block.fence)}"` : '';
|
|
1461
1636
|
replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${block.code}</code></pre>`;
|
|
1462
1637
|
}
|
|
1463
|
-
|
|
1638
|
+
|
|
1464
1639
|
const placeholder = `${PLACEHOLDER_CB}${i}§`;
|
|
1465
1640
|
html = html.replace(placeholder, replacement);
|
|
1466
1641
|
});
|
|
1467
|
-
|
|
1468
|
-
// Restore inline code
|
|
1642
|
+
|
|
1643
|
+
// Restore inline code spans
|
|
1469
1644
|
inlineCodes.forEach((code, i) => {
|
|
1470
1645
|
const placeholder = `${PLACEHOLDER_IC}${i}§`;
|
|
1471
1646
|
html = html.replace(placeholder, `<code${getAttr('code')}${dataQd('`')}>${code}</code>`);
|
|
1472
1647
|
});
|
|
1473
|
-
|
|
1648
|
+
|
|
1474
1649
|
return html.trim();
|
|
1475
1650
|
}
|
|
1476
1651
|
|
|
1652
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1653
|
+
// Block-level line scanner
|
|
1654
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* scanLineBlocks — single-pass line scanner for headings, HR, blockquotes
|
|
1658
|
+
*
|
|
1659
|
+
* Walks the text line by line. For each line it checks (in order):
|
|
1660
|
+
* 1. Heading — starts with 1-6 '#' followed by a space
|
|
1661
|
+
* 2. HR — line is entirely '---…' (3+ dashes, optional trailing space)
|
|
1662
|
+
* 3. Blockquote — starts with '> ' (the > was already HTML-escaped)
|
|
1663
|
+
*
|
|
1664
|
+
* Lines that don't match any block pattern are passed through unchanged.
|
|
1665
|
+
*
|
|
1666
|
+
* This replaces three separate global regex passes from the pre-1.2.8
|
|
1667
|
+
* architecture with one structured scan.
|
|
1668
|
+
*
|
|
1669
|
+
* @param {string} text The document text (HTML-escaped, code extracted)
|
|
1670
|
+
* @param {Function} getAttr Attribute factory (class or style)
|
|
1671
|
+
* @param {Function} dataQd Bidirectional marker factory
|
|
1672
|
+
* @returns {string} Text with block-level elements rendered
|
|
1673
|
+
*/
|
|
1674
|
+
function scanLineBlocks(text, getAttr, dataQd) {
|
|
1675
|
+
const lines = text.split('\n');
|
|
1676
|
+
const result = [];
|
|
1677
|
+
let i = 0;
|
|
1678
|
+
|
|
1679
|
+
while (i < lines.length) {
|
|
1680
|
+
const line = lines[i];
|
|
1681
|
+
|
|
1682
|
+
// ── Heading ──
|
|
1683
|
+
// Count leading '#' characters. Valid heading: 1-6 hashes then a space.
|
|
1684
|
+
// Example: "## Hello World ##" → <h2>Hello World</h2>
|
|
1685
|
+
let hashCount = 0;
|
|
1686
|
+
while (hashCount < line.length && hashCount < 7 && line[hashCount] === '#') {
|
|
1687
|
+
hashCount++;
|
|
1688
|
+
}
|
|
1689
|
+
if (hashCount >= 1 && hashCount <= 6 && line[hashCount] === ' ') {
|
|
1690
|
+
// Extract content after "# " and strip trailing hashes
|
|
1691
|
+
const content = line.slice(hashCount + 1).replace(/\s*#+\s*$/, '');
|
|
1692
|
+
const tag = 'h' + hashCount;
|
|
1693
|
+
result.push(`<${tag}${getAttr(tag)}${dataQd('#'.repeat(hashCount))}>${content}</${tag}>`);
|
|
1694
|
+
i++;
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// ── Horizontal Rule ──
|
|
1699
|
+
// Three or more dashes, optional trailing whitespace, nothing else.
|
|
1700
|
+
if (isDashHRLine(line)) {
|
|
1701
|
+
result.push(`<hr${getAttr('hr')}>`);
|
|
1702
|
+
i++;
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// ── Blockquote ──
|
|
1707
|
+
// After Phase 2, the '>' character has been escaped to '>'.
|
|
1708
|
+
// Pattern: "> content" or merged consecutive blockquotes.
|
|
1709
|
+
if (/^>\s+/.test(line)) {
|
|
1710
|
+
result.push(`<blockquote${getAttr('blockquote')}>${line.replace(/^>\s+/, '')}</blockquote>`);
|
|
1711
|
+
i++;
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// ── Pass-through ──
|
|
1716
|
+
result.push(line);
|
|
1717
|
+
i++;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// Merge consecutive blockquotes into a single element.
|
|
1721
|
+
// <blockquote>A</blockquote>\n<blockquote>B</blockquote>
|
|
1722
|
+
// → <blockquote>A\nB</blockquote>
|
|
1723
|
+
let joined = result.join('\n');
|
|
1724
|
+
joined = joined.replace(/<\/blockquote>\n<blockquote>/g, '\n');
|
|
1725
|
+
return joined;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1729
|
+
// Table processing (line walker)
|
|
1730
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1731
|
+
|
|
1477
1732
|
/**
|
|
1478
|
-
*
|
|
1733
|
+
* Inline markdown formatter for table cells.
|
|
1734
|
+
* Handles bold, italic, strikethrough, and code within cell text.
|
|
1735
|
+
* Links / images / autolinks are handled by the global inline pass
|
|
1736
|
+
* (Phase 3 Step 4) which runs after table processing.
|
|
1479
1737
|
*/
|
|
1480
1738
|
function processInlineMarkdown(text, getAttr) {
|
|
1481
|
-
|
|
1482
|
-
// Process inline formatting patterns
|
|
1483
1739
|
const patterns = [
|
|
1484
1740
|
[/\*\*(.+?)\*\*/g, 'strong'],
|
|
1485
1741
|
[/__(.+?)__/g, 'strong'],
|
|
@@ -1488,27 +1744,32 @@ function processInlineMarkdown(text, getAttr) {
|
|
|
1488
1744
|
[/~~(.+?)~~/g, 'del'],
|
|
1489
1745
|
[/`([^`]+)`/g, 'code']
|
|
1490
1746
|
];
|
|
1491
|
-
|
|
1492
1747
|
patterns.forEach(([pattern, tag]) => {
|
|
1493
1748
|
text = text.replace(pattern, `<${tag}${getAttr(tag)}>$1</${tag}>`);
|
|
1494
1749
|
});
|
|
1495
|
-
|
|
1496
1750
|
return text;
|
|
1497
1751
|
}
|
|
1498
1752
|
|
|
1499
1753
|
/**
|
|
1500
|
-
*
|
|
1754
|
+
* processTable — line walker for markdown tables
|
|
1755
|
+
*
|
|
1756
|
+
* Walks through lines looking for runs of pipe-containing lines.
|
|
1757
|
+
* Each run is validated (must contain a separator row: |---|---|)
|
|
1758
|
+
* and rendered as an HTML <table>. Invalid runs are restored as-is.
|
|
1759
|
+
*
|
|
1760
|
+
* @param {string} text Full document text
|
|
1761
|
+
* @param {Function} getAttr Attribute factory
|
|
1762
|
+
* @returns {string} Text with tables rendered
|
|
1501
1763
|
*/
|
|
1502
1764
|
function processTable(text, getAttr) {
|
|
1503
1765
|
const lines = text.split('\n');
|
|
1504
1766
|
const result = [];
|
|
1505
1767
|
let inTable = false;
|
|
1506
1768
|
let tableLines = [];
|
|
1507
|
-
|
|
1769
|
+
|
|
1508
1770
|
for (let i = 0; i < lines.length; i++) {
|
|
1509
1771
|
const line = lines[i].trim();
|
|
1510
|
-
|
|
1511
|
-
// Check if this line looks like a table row (with or without trailing |)
|
|
1772
|
+
|
|
1512
1773
|
if (line.includes('|') && (line.startsWith('|') || /[^\\|]/.test(line))) {
|
|
1513
1774
|
if (!inTable) {
|
|
1514
1775
|
inTable = true;
|
|
@@ -1516,14 +1777,11 @@ function processTable(text, getAttr) {
|
|
|
1516
1777
|
}
|
|
1517
1778
|
tableLines.push(line);
|
|
1518
1779
|
} else {
|
|
1519
|
-
// Not a table line
|
|
1520
1780
|
if (inTable) {
|
|
1521
|
-
// Process the accumulated table
|
|
1522
1781
|
const tableHtml = buildTable(tableLines, getAttr);
|
|
1523
1782
|
if (tableHtml) {
|
|
1524
1783
|
result.push(tableHtml);
|
|
1525
1784
|
} else {
|
|
1526
|
-
// Not a valid table, restore original lines
|
|
1527
1785
|
result.push(...tableLines);
|
|
1528
1786
|
}
|
|
1529
1787
|
inTable = false;
|
|
@@ -1532,8 +1790,8 @@ function processTable(text, getAttr) {
|
|
|
1532
1790
|
result.push(lines[i]);
|
|
1533
1791
|
}
|
|
1534
1792
|
}
|
|
1535
|
-
|
|
1536
|
-
// Handle table at end of
|
|
1793
|
+
|
|
1794
|
+
// Handle table at end of document
|
|
1537
1795
|
if (inTable && tableLines.length > 0) {
|
|
1538
1796
|
const tableHtml = buildTable(tableLines, getAttr);
|
|
1539
1797
|
if (tableHtml) {
|
|
@@ -1542,35 +1800,35 @@ function processTable(text, getAttr) {
|
|
|
1542
1800
|
result.push(...tableLines);
|
|
1543
1801
|
}
|
|
1544
1802
|
}
|
|
1545
|
-
|
|
1803
|
+
|
|
1546
1804
|
return result.join('\n');
|
|
1547
1805
|
}
|
|
1548
1806
|
|
|
1549
1807
|
/**
|
|
1550
|
-
*
|
|
1808
|
+
* buildTable — validate and render a table from accumulated lines
|
|
1809
|
+
*
|
|
1810
|
+
* @param {string[]} lines Array of pipe-containing lines
|
|
1811
|
+
* @param {Function} getAttr Attribute factory
|
|
1812
|
+
* @returns {string|null} HTML table string, or null if invalid
|
|
1551
1813
|
*/
|
|
1552
1814
|
function buildTable(lines, getAttr) {
|
|
1553
|
-
|
|
1554
1815
|
if (lines.length < 2) return null;
|
|
1555
|
-
|
|
1556
|
-
//
|
|
1816
|
+
|
|
1817
|
+
// Find the separator row (---|---|)
|
|
1557
1818
|
let separatorIndex = -1;
|
|
1558
1819
|
for (let i = 1; i < lines.length; i++) {
|
|
1559
|
-
// Support separator with or without leading/trailing pipes
|
|
1560
1820
|
if (/^\|?[\s\-:|]+\|?$/.test(lines[i]) && lines[i].includes('-')) {
|
|
1561
1821
|
separatorIndex = i;
|
|
1562
1822
|
break;
|
|
1563
1823
|
}
|
|
1564
1824
|
}
|
|
1565
|
-
|
|
1566
1825
|
if (separatorIndex === -1) return null;
|
|
1567
|
-
|
|
1826
|
+
|
|
1568
1827
|
const headerLines = lines.slice(0, separatorIndex);
|
|
1569
1828
|
const bodyLines = lines.slice(separatorIndex + 1);
|
|
1570
|
-
|
|
1571
|
-
// Parse alignment from separator
|
|
1829
|
+
|
|
1830
|
+
// Parse alignment from separator cells (:--- = left, :---: = center, ---: = right)
|
|
1572
1831
|
const separator = lines[separatorIndex];
|
|
1573
|
-
// Handle pipes at start/end or not
|
|
1574
1832
|
const separatorCells = separator.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
|
|
1575
1833
|
const alignments = separatorCells.map(cell => {
|
|
1576
1834
|
const trimmed = cell.trim();
|
|
@@ -1578,31 +1836,28 @@ function buildTable(lines, getAttr) {
|
|
|
1578
1836
|
if (trimmed.endsWith(':')) return 'right';
|
|
1579
1837
|
return 'left';
|
|
1580
1838
|
});
|
|
1581
|
-
|
|
1839
|
+
|
|
1582
1840
|
let html = `<table${getAttr('table')}>\n`;
|
|
1583
|
-
|
|
1584
|
-
//
|
|
1585
|
-
// Note: headerLines will always have length > 0 since separatorIndex starts from 1
|
|
1841
|
+
|
|
1842
|
+
// Header
|
|
1586
1843
|
html += `<thead${getAttr('thead')}>\n`;
|
|
1587
1844
|
headerLines.forEach(line => {
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
html += '</tr>\n';
|
|
1845
|
+
html += `<tr${getAttr('tr')}>\n`;
|
|
1846
|
+
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
|
|
1847
|
+
cells.forEach((cell, i) => {
|
|
1848
|
+
const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
|
|
1849
|
+
const processedCell = processInlineMarkdown(cell.trim(), getAttr);
|
|
1850
|
+
html += `<th${getAttr('th', alignStyle)}>${processedCell}</th>\n`;
|
|
1851
|
+
});
|
|
1852
|
+
html += '</tr>\n';
|
|
1597
1853
|
});
|
|
1598
1854
|
html += '</thead>\n';
|
|
1599
|
-
|
|
1600
|
-
//
|
|
1855
|
+
|
|
1856
|
+
// Body
|
|
1601
1857
|
if (bodyLines.length > 0) {
|
|
1602
1858
|
html += `<tbody${getAttr('tbody')}>\n`;
|
|
1603
1859
|
bodyLines.forEach(line => {
|
|
1604
1860
|
html += `<tr${getAttr('tr')}>\n`;
|
|
1605
|
-
// Handle pipes at start/end or not
|
|
1606
1861
|
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
|
|
1607
1862
|
cells.forEach((cell, i) => {
|
|
1608
1863
|
const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
|
|
@@ -1613,61 +1868,81 @@ function buildTable(lines, getAttr) {
|
|
|
1613
1868
|
});
|
|
1614
1869
|
html += '</tbody>\n';
|
|
1615
1870
|
}
|
|
1616
|
-
|
|
1871
|
+
|
|
1617
1872
|
html += '</table>';
|
|
1618
1873
|
return html;
|
|
1619
1874
|
}
|
|
1620
1875
|
|
|
1876
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1877
|
+
// List processing (line walker)
|
|
1878
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1879
|
+
|
|
1621
1880
|
/**
|
|
1622
|
-
*
|
|
1881
|
+
* processLists — line walker for ordered, unordered, and task lists
|
|
1882
|
+
*
|
|
1883
|
+
* Scans each line for list markers (-, *, +, 1., 2., etc.) with
|
|
1884
|
+
* optional leading indentation for nesting. Non-list lines close
|
|
1885
|
+
* any open lists and pass through unchanged.
|
|
1886
|
+
*
|
|
1887
|
+
* Task lists (- [ ] / - [x]) are detected and rendered with
|
|
1888
|
+
* checkbox inputs.
|
|
1889
|
+
*
|
|
1890
|
+
* @param {string} text Full document text
|
|
1891
|
+
* @param {Function} getAttr Attribute factory
|
|
1892
|
+
* @param {boolean} inline_styles Whether to use inline styles
|
|
1893
|
+
* @param {boolean} bidirectional Whether to add data-qd markers
|
|
1894
|
+
* @returns {string} Text with lists rendered
|
|
1623
1895
|
*/
|
|
1624
1896
|
function processLists(text, getAttr, inline_styles, bidirectional) {
|
|
1625
|
-
|
|
1626
1897
|
const lines = text.split('\n');
|
|
1627
1898
|
const result = [];
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
// Helper to escape HTML for data-qd attributes
|
|
1631
|
-
|
|
1899
|
+
const listStack = []; // tracks nesting: [{type:'ul', level:0}, …]
|
|
1900
|
+
|
|
1901
|
+
// Helper to escape HTML for data-qd attributes. List markers (`-`, `*`,
|
|
1902
|
+
// `+`, `1.`, etc.) never contain HTML-special chars, so the replace
|
|
1903
|
+
// callback is defensive-only and never actually fires in practice.
|
|
1904
|
+
/* istanbul ignore next - defensive: list markers never trigger escaping */
|
|
1905
|
+
const escapeHtml = (text) => text.replace(/[&<>"']/g,
|
|
1906
|
+
/* istanbul ignore next - defensive: list markers never contain HTML specials */
|
|
1907
|
+
m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]);
|
|
1908
|
+
/* istanbul ignore next - trivial no-op fallback; not exercised via bd bundle */
|
|
1632
1909
|
const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
|
|
1633
|
-
|
|
1910
|
+
|
|
1634
1911
|
for (let i = 0; i < lines.length; i++) {
|
|
1635
1912
|
const line = lines[i];
|
|
1636
1913
|
const match = line.match(/^(\s*)([*\-+]|\d+\.)\s+(.+)$/);
|
|
1637
|
-
|
|
1914
|
+
|
|
1638
1915
|
if (match) {
|
|
1639
1916
|
const [, indent, marker, content] = match;
|
|
1640
1917
|
const level = Math.floor(indent.length / 2);
|
|
1641
1918
|
const isOrdered = /^\d+\./.test(marker);
|
|
1642
1919
|
const listType = isOrdered ? 'ol' : 'ul';
|
|
1643
|
-
|
|
1644
|
-
//
|
|
1920
|
+
|
|
1921
|
+
// Task list detection (only in unordered lists)
|
|
1645
1922
|
let listItemContent = content;
|
|
1646
1923
|
let taskListClass = '';
|
|
1647
1924
|
const taskMatch = content.match(/^\[([x ])\]\s+(.*)$/i);
|
|
1648
1925
|
if (taskMatch && !isOrdered) {
|
|
1649
1926
|
const [, checked, taskContent] = taskMatch;
|
|
1650
1927
|
const isChecked = checked.toLowerCase() === 'x';
|
|
1651
|
-
const checkboxAttr = inline_styles
|
|
1652
|
-
? ' style="margin-right:.5em"'
|
|
1928
|
+
const checkboxAttr = inline_styles
|
|
1929
|
+
? ' style="margin-right:.5em"'
|
|
1653
1930
|
: ` class="${CLASS_PREFIX}task-checkbox"`;
|
|
1654
1931
|
listItemContent = `<input type="checkbox"${checkboxAttr}${isChecked ? ' checked' : ''} disabled> ${taskContent}`;
|
|
1655
1932
|
taskListClass = inline_styles ? ' style="list-style:none"' : ` class="${CLASS_PREFIX}task-item"`;
|
|
1656
1933
|
}
|
|
1657
|
-
|
|
1658
|
-
// Close deeper levels
|
|
1934
|
+
|
|
1935
|
+
// Close deeper nesting levels
|
|
1659
1936
|
while (listStack.length > level + 1) {
|
|
1660
1937
|
const list = listStack.pop();
|
|
1661
1938
|
result.push(`</${list.type}>`);
|
|
1662
1939
|
}
|
|
1663
|
-
|
|
1664
|
-
// Open new
|
|
1940
|
+
|
|
1941
|
+
// Open new list or switch type at current level
|
|
1665
1942
|
if (listStack.length === level) {
|
|
1666
|
-
// Need to open a new list
|
|
1667
1943
|
listStack.push({ type: listType, level });
|
|
1668
1944
|
result.push(`<${listType}${getAttr(listType)}>`);
|
|
1669
1945
|
} else if (listStack.length === level + 1) {
|
|
1670
|
-
// Check if we need to switch list type
|
|
1671
1946
|
const currentList = listStack[listStack.length - 1];
|
|
1672
1947
|
if (currentList.type !== listType) {
|
|
1673
1948
|
result.push(`</${currentList.type}>`);
|
|
@@ -1676,11 +1951,11 @@ function processLists(text, getAttr, inline_styles, bidirectional) {
|
|
|
1676
1951
|
result.push(`<${listType}${getAttr(listType)}>`);
|
|
1677
1952
|
}
|
|
1678
1953
|
}
|
|
1679
|
-
|
|
1954
|
+
|
|
1680
1955
|
const liAttr = taskListClass || getAttr('li');
|
|
1681
1956
|
result.push(`<li${liAttr}${dataQd(marker)}>${listItemContent}</li>`);
|
|
1682
1957
|
} else {
|
|
1683
|
-
// Not a list item
|
|
1958
|
+
// Not a list item — close all open lists
|
|
1684
1959
|
while (listStack.length > 0) {
|
|
1685
1960
|
const list = listStack.pop();
|
|
1686
1961
|
result.push(`</${list.type}>`);
|
|
@@ -1688,76 +1963,76 @@ function processLists(text, getAttr, inline_styles, bidirectional) {
|
|
|
1688
1963
|
result.push(line);
|
|
1689
1964
|
}
|
|
1690
1965
|
}
|
|
1691
|
-
|
|
1692
|
-
// Close any remaining lists
|
|
1966
|
+
|
|
1967
|
+
// Close any remaining open lists
|
|
1693
1968
|
while (listStack.length > 0) {
|
|
1694
1969
|
const list = listStack.pop();
|
|
1695
1970
|
result.push(`</${list.type}>`);
|
|
1696
1971
|
}
|
|
1697
|
-
|
|
1972
|
+
|
|
1698
1973
|
return result.join('\n');
|
|
1699
1974
|
}
|
|
1700
1975
|
|
|
1976
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1977
|
+
// Static API
|
|
1978
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1979
|
+
|
|
1701
1980
|
/**
|
|
1702
|
-
* Emit CSS
|
|
1703
|
-
*
|
|
1704
|
-
* @param {string}
|
|
1705
|
-
* @
|
|
1981
|
+
* Emit CSS rules for all quikdown elements.
|
|
1982
|
+
*
|
|
1983
|
+
* @param {string} prefix Class prefix (default: 'quikdown-')
|
|
1984
|
+
* @param {string} theme 'light' (default) or 'dark'
|
|
1985
|
+
* @returns {string} CSS text
|
|
1706
1986
|
*/
|
|
1707
1987
|
quikdown.emitStyles = function(prefix = 'quikdown-', theme = 'light') {
|
|
1708
1988
|
const styles = QUIKDOWN_STYLES;
|
|
1709
|
-
|
|
1710
|
-
// Define theme color overrides
|
|
1989
|
+
|
|
1711
1990
|
const themeOverrides = {
|
|
1712
1991
|
dark: {
|
|
1713
|
-
'#f4f4f4': '#2a2a2a',
|
|
1714
|
-
'#f0f0f0': '#2a2a2a',
|
|
1715
|
-
'#f2f2f2': '#2a2a2a',
|
|
1716
|
-
'#ddd': '#3a3a3a',
|
|
1717
|
-
'#06c': '#6db3f2',
|
|
1992
|
+
'#f4f4f4': '#2a2a2a', // pre background
|
|
1993
|
+
'#f0f0f0': '#2a2a2a', // code background
|
|
1994
|
+
'#f2f2f2': '#2a2a2a', // th background
|
|
1995
|
+
'#ddd': '#3a3a3a', // borders
|
|
1996
|
+
'#06c': '#6db3f2', // links
|
|
1718
1997
|
_textColor: '#e0e0e0'
|
|
1719
1998
|
},
|
|
1720
1999
|
light: {
|
|
1721
|
-
_textColor: '#333'
|
|
2000
|
+
_textColor: '#333'
|
|
1722
2001
|
}
|
|
1723
2002
|
};
|
|
1724
|
-
|
|
2003
|
+
|
|
1725
2004
|
let css = '';
|
|
1726
2005
|
for (const [tag, style] of Object.entries(styles)) {
|
|
1727
2006
|
let themedStyle = style;
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
if (!oldColor.startsWith('_')) {
|
|
1734
|
-
themedStyle = themedStyle.replace(new RegExp(oldColor, 'g'), newColor);
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
// Add text color for certain elements in dark theme
|
|
1739
|
-
const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
|
|
1740
|
-
if (needsTextColor.includes(tag)) {
|
|
1741
|
-
themedStyle += `;color:${themeOverrides.dark._textColor}`;
|
|
1742
|
-
}
|
|
1743
|
-
} else if (theme === 'light' && themeOverrides.light) {
|
|
1744
|
-
// Add explicit text color for light theme elements too
|
|
1745
|
-
const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
|
|
1746
|
-
if (needsTextColor.includes(tag)) {
|
|
1747
|
-
themedStyle += `;color:${themeOverrides.light._textColor}`;
|
|
2007
|
+
|
|
2008
|
+
if (theme === 'dark' && themeOverrides.dark) {
|
|
2009
|
+
for (const [oldColor, newColor] of Object.entries(themeOverrides.dark)) {
|
|
2010
|
+
if (!oldColor.startsWith('_')) {
|
|
2011
|
+
themedStyle = themedStyle.replaceAll(oldColor, newColor);
|
|
1748
2012
|
}
|
|
1749
2013
|
}
|
|
1750
|
-
|
|
2014
|
+
const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
|
|
2015
|
+
if (needsTextColor.includes(tag)) {
|
|
2016
|
+
themedStyle += `;color:${themeOverrides.dark._textColor}`;
|
|
2017
|
+
}
|
|
2018
|
+
} else if (theme === 'light' && themeOverrides.light) {
|
|
2019
|
+
const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
|
|
2020
|
+
if (needsTextColor.includes(tag)) {
|
|
2021
|
+
themedStyle += `;color:${themeOverrides.light._textColor}`;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
1751
2025
|
css += `.${prefix}${tag} { ${themedStyle} }\n`;
|
|
1752
2026
|
}
|
|
1753
|
-
|
|
2027
|
+
|
|
1754
2028
|
return css;
|
|
1755
2029
|
};
|
|
1756
2030
|
|
|
1757
2031
|
/**
|
|
1758
|
-
*
|
|
1759
|
-
*
|
|
1760
|
-
* @
|
|
2032
|
+
* Create a pre-configured parser with baked-in options.
|
|
2033
|
+
*
|
|
2034
|
+
* @param {Object} options Options to bake in
|
|
2035
|
+
* @returns {Function} Configured quikdown(markdown) function
|
|
1761
2036
|
*/
|
|
1762
2037
|
quikdown.configure = function(options) {
|
|
1763
2038
|
return function(markdown) {
|
|
@@ -1765,18 +2040,18 @@ quikdown.configure = function(options) {
|
|
|
1765
2040
|
};
|
|
1766
2041
|
};
|
|
1767
2042
|
|
|
1768
|
-
/**
|
|
1769
|
-
* Version information
|
|
1770
|
-
*/
|
|
2043
|
+
/** Semantic version (injected at build time) */
|
|
1771
2044
|
quikdown.version = quikdownVersion;
|
|
1772
2045
|
|
|
1773
|
-
//
|
|
2046
|
+
// ════════════════════════════════════════════════════════════════════
|
|
2047
|
+
// Exports
|
|
2048
|
+
// ════════════════════════════════════════════════════════════════════
|
|
2049
|
+
|
|
1774
2050
|
/* istanbul ignore next */
|
|
1775
2051
|
if (typeof module !== 'undefined' && module.exports) {
|
|
1776
2052
|
module.exports = quikdown;
|
|
1777
2053
|
}
|
|
1778
2054
|
|
|
1779
|
-
// For browser global
|
|
1780
2055
|
/* istanbul ignore next */
|
|
1781
2056
|
if (typeof window !== 'undefined') {
|
|
1782
2057
|
window.quikdown = quikdown;
|
|
@@ -1800,8 +2075,11 @@ function quikdown_bd(markdown, options = {}) {
|
|
|
1800
2075
|
return quikdown(markdown, { ...options, bidirectional: true });
|
|
1801
2076
|
}
|
|
1802
2077
|
|
|
1803
|
-
// Copy all properties and methods from quikdown (including version)
|
|
2078
|
+
// Copy all properties and methods from quikdown (including version).
|
|
2079
|
+
// Skip `configure` — quikdown_bd provides its own override below, so the
|
|
2080
|
+
// inner quikdown.configure is dead code in this bundle.
|
|
1804
2081
|
Object.keys(quikdown).forEach(key => {
|
|
2082
|
+
if (key === 'configure') return;
|
|
1805
2083
|
quikdown_bd[key] = quikdown[key];
|
|
1806
2084
|
});
|
|
1807
2085
|
|
|
@@ -1835,7 +2113,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
1835
2113
|
|
|
1836
2114
|
// Process children with context
|
|
1837
2115
|
let childContent = '';
|
|
1838
|
-
for (
|
|
2116
|
+
for (const child of node.childNodes) {
|
|
1839
2117
|
childContent += walkNode(child, { parentTag: tag, ...parentContext });
|
|
1840
2118
|
}
|
|
1841
2119
|
|
|
@@ -2069,7 +2347,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
2069
2347
|
let index = 1;
|
|
2070
2348
|
const indent = ' '.repeat(depth);
|
|
2071
2349
|
|
|
2072
|
-
for (
|
|
2350
|
+
for (const child of listNode.children) {
|
|
2073
2351
|
if (child.tagName !== 'LI') continue;
|
|
2074
2352
|
|
|
2075
2353
|
const dataQd = child.getAttribute('data-qd');
|
|
@@ -2082,7 +2360,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
2082
2360
|
marker = '-';
|
|
2083
2361
|
// Get text without the checkbox
|
|
2084
2362
|
let text = '';
|
|
2085
|
-
for (
|
|
2363
|
+
for (const node of child.childNodes) {
|
|
2086
2364
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
2087
2365
|
text += node.textContent;
|
|
2088
2366
|
} else if (node.tagName && node.tagName !== 'INPUT') {
|
|
@@ -2093,7 +2371,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
2093
2371
|
} else {
|
|
2094
2372
|
let itemContent = '';
|
|
2095
2373
|
|
|
2096
|
-
for (
|
|
2374
|
+
for (const node of child.childNodes) {
|
|
2097
2375
|
if (node.tagName === 'UL' || node.tagName === 'OL') {
|
|
2098
2376
|
itemContent += walkList(node, node.tagName === 'OL', depth + 1);
|
|
2099
2377
|
} else {
|
|
@@ -2122,7 +2400,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
2122
2400
|
const headerRow = thead.querySelector('tr');
|
|
2123
2401
|
if (headerRow) {
|
|
2124
2402
|
const headers = [];
|
|
2125
|
-
for (
|
|
2403
|
+
for (const th of headerRow.querySelectorAll('th')) {
|
|
2126
2404
|
headers.push(th.textContent.trim());
|
|
2127
2405
|
}
|
|
2128
2406
|
result += '| ' + headers.join(' | ') + ' |\n';
|
|
@@ -2141,9 +2419,9 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
2141
2419
|
// Process body
|
|
2142
2420
|
const tbody = table.querySelector('tbody');
|
|
2143
2421
|
if (tbody) {
|
|
2144
|
-
for (
|
|
2422
|
+
for (const row of tbody.querySelectorAll('tr')) {
|
|
2145
2423
|
const cells = [];
|
|
2146
|
-
for (
|
|
2424
|
+
for (const td of row.querySelectorAll('td')) {
|
|
2147
2425
|
cells.push(td.textContent.trim());
|
|
2148
2426
|
}
|
|
2149
2427
|
if (cells.length > 0) {
|
|
@@ -2165,10 +2443,13 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
2165
2443
|
return markdown;
|
|
2166
2444
|
};
|
|
2167
2445
|
|
|
2168
|
-
// Override the configure method to return a bidirectional version
|
|
2446
|
+
// Override the configure method to return a bidirectional version.
|
|
2447
|
+
// We delegate to the inner quikdown.configure so the shared closure
|
|
2448
|
+
// machinery is exercised in both bundles (no dead code).
|
|
2169
2449
|
quikdown_bd.configure = function(options) {
|
|
2450
|
+
const innerParser = quikdown.configure({ ...options, bidirectional: true });
|
|
2170
2451
|
return function(markdown) {
|
|
2171
|
-
return
|
|
2452
|
+
return innerParser(markdown);
|
|
2172
2453
|
};
|
|
2173
2454
|
};
|
|
2174
2455
|
|