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