bitwrench 1.2.16 → 2.0.7

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.
Files changed (130) hide show
  1. package/README.md +160 -158
  2. package/bin/bitwrench.js +3 -0
  3. package/dist/bitwrench-code-edit.cjs.js +639 -0
  4. package/dist/bitwrench-code-edit.es5.js +875 -0
  5. package/dist/bitwrench-code-edit.es5.min.js +15 -0
  6. package/dist/bitwrench-code-edit.esm.js +628 -0
  7. package/dist/bitwrench-code-edit.esm.min.js +15 -0
  8. package/dist/bitwrench-code-edit.umd.js +645 -0
  9. package/dist/bitwrench-code-edit.umd.min.js +15 -0
  10. package/dist/bitwrench.cjs.js +6983 -0
  11. package/dist/bitwrench.cjs.min.js +62 -0
  12. package/dist/bitwrench.css +5100 -0
  13. package/dist/bitwrench.es5.js +8446 -0
  14. package/dist/bitwrench.es5.min.js +31 -0
  15. package/dist/bitwrench.esm.js +6981 -0
  16. package/dist/bitwrench.esm.min.js +62 -0
  17. package/dist/bitwrench.umd.js +6989 -0
  18. package/dist/bitwrench.umd.min.js +62 -0
  19. package/dist/builds.json +127 -0
  20. package/dist/sri.json +18 -0
  21. package/package.json +86 -24
  22. package/readme.html +288 -0
  23. package/src/bitwrench-code-edit.js +627 -0
  24. package/src/bitwrench-color-utils.js +311 -0
  25. package/src/bitwrench-component-base.js +736 -0
  26. package/src/bitwrench-components-inline.js +374 -0
  27. package/src/bitwrench-components-v2.js +1879 -0
  28. package/src/bitwrench-components.js +610 -0
  29. package/src/bitwrench-styles.js +3240 -0
  30. package/src/bitwrench.js +3367 -0
  31. package/src/cli/convert.js +205 -0
  32. package/src/cli/index.js +122 -0
  33. package/src/cli/inject.js +55 -0
  34. package/src/cli/layout-default.js +142 -0
  35. package/src/generate-css.js +381 -0
  36. package/src/vendor/quikdown.js +654 -0
  37. package/src/version.js +16 -0
  38. package/.eslintrc.json +0 -27
  39. package/.github/workflows/codeql-analysis.yml +0 -72
  40. package/.travis.yml +0 -34
  41. package/bitwrench.css +0 -92
  42. package/bitwrench.js +0 -3348
  43. package/bitwrench.js_sri.txt +0 -1
  44. package/bitwrench.min.js +0 -1
  45. package/bitwrench.min.js_sri.txt +0 -1
  46. package/bitwrench_ESM.js +0 -3207
  47. package/bitwrench_ESM.js_sri.txt +0 -1
  48. package/bitwrench_ESM.min.js +0 -1
  49. package/bitwrench_ESM.min.js_sri.txt +0 -1
  50. package/dev/bitwrench-todo.md +0 -215
  51. package/dev/css-arrows.md +0 -23
  52. package/dev/docStringDev.js +0 -124
  53. package/dev/docStringParseDev.js +0 -171
  54. package/dev/example11-load-mjs-page.html +0 -17
  55. package/dev/figures.html +0 -37
  56. package/dev/html_gen.js +0 -349
  57. package/dev/htmld.md +0 -250
  58. package/dev/htmldev.html +0 -45
  59. package/dev/index-old.html +0 -87
  60. package/dev/misc-notes.md +0 -21
  61. package/dev/norm.css +0 -30
  62. package/dev/notes.md +0 -2
  63. package/dev/pageData.mjs +0 -69
  64. package/dev/sizes.html +0 -49
  65. package/dev/universal-js-module.js +0 -37
  66. package/examples/example1.html +0 -78
  67. package/examples/example10.html +0 -84
  68. package/examples/example11.html +0 -17
  69. package/examples/example12.html +0 -18
  70. package/examples/example2.html +0 -44
  71. package/examples/example3.html +0 -50
  72. package/examples/example4.html +0 -22
  73. package/examples/example5.html +0 -82
  74. package/examples/example6.html +0 -128
  75. package/examples/example7.html +0 -91
  76. package/examples/example8.html +0 -27
  77. package/examples/example9.html +0 -102
  78. package/examples/examplePageData12.mjs +0 -73
  79. package/examples/pageData.mjs +0 -69
  80. package/examples/pico.min.css +0 -5
  81. package/icon/bitwrench-dark-tall.png +0 -0
  82. package/icon/bitwrench-dark.png +0 -0
  83. package/icon/bitwrench-icon-lt-grey.png +0 -0
  84. package/icon/bitwrench-icon.vsd +0 -0
  85. package/icon/bitwrench-logo-dark.png +0 -0
  86. package/icon/bitwrench-logo-full.png +0 -0
  87. package/icon/bitwrench-logo-green.png +0 -0
  88. package/icon/bitwrench-logo-grey.png +0 -0
  89. package/icon/bitwrench-logo-white.png +0 -0
  90. package/icon/bitwrench-logos-colors.png +0 -0
  91. package/icon/bitwrench-thick-logo.png +0 -0
  92. package/icon/bitwrench-thick-teal/android-chrome-192x192.png +0 -0
  93. package/icon/bitwrench-thick-teal/android-chrome-512x512.png +0 -0
  94. package/icon/bitwrench-thick-teal/apple-touch-icon.png +0 -0
  95. package/icon/bitwrench-thick-teal/browserconfig.xml +0 -9
  96. package/icon/bitwrench-thick-teal/favicon-16x16.png +0 -0
  97. package/icon/bitwrench-thick-teal/favicon-32x32.png +0 -0
  98. package/icon/bitwrench-thick-teal/favicon.ico +0 -0
  99. package/icon/bitwrench-thick-teal/mstile-144x144.png +0 -0
  100. package/icon/bitwrench-thick-teal/mstile-150x150.png +0 -0
  101. package/icon/bitwrench-thick-teal/mstile-310x150.png +0 -0
  102. package/icon/bitwrench-thick-teal/mstile-310x310.png +0 -0
  103. package/icon/bitwrench-thick-teal/mstile-70x70.png +0 -0
  104. package/icon/bitwrench-thick-teal/site.webmanifest +0 -19
  105. package/icon/bitwrench-thick-teal.ico +0 -0
  106. package/icon/bitwrench-thick-teal.svg +0 -44
  107. package/icon/bitwrench-thick-teal.zip +0 -0
  108. package/icon/favicon-test.html +0 -20
  109. package/icon/logos-test.PNG +0 -0
  110. package/images/bitwrench-512x512.png +0 -0
  111. package/images/bitwrench-logo-med.png +0 -0
  112. package/images/bitwrench-thick-logo.png +0 -0
  113. package/images/bitwrench-thick-logo.svg +0 -64
  114. package/images/bitwrench-thick-teal.ico +0 -0
  115. package/images/favicon.ico +0 -0
  116. package/index.html +0 -282
  117. package/instr_tmp/bitwrench.js +0 -1350
  118. package/karma.conf.js +0 -140
  119. package/makefile +0 -21
  120. package/quick-docs.html +0 -206
  121. package/test/bitwrench_test.js +0 -1255
  122. package/test/karma-test.js +0 -1081
  123. package/tools/bw_deprecatedNames.js +0 -19
  124. package/tools/bwconsole.js +0 -20
  125. package/tools/createSimpleHTMLPage.js +0 -41
  126. package/tools/emitreadme.sh +0 -4
  127. package/tools/export-bw-default-css.js +0 -41
  128. package/tools/umd2ModuleHack.js +0 -32
  129. package/tools/update-bw-package.js +0 -36
  130. package/tools/updatereadme.js +0 -34
@@ -0,0 +1,654 @@
1
+ /**
2
+ * Vendored: quikdown v1.2.0
3
+ * Lightweight Markdown Parser - https://github.com/deftio/quikdown
4
+ * @license BSD-2-Clause
5
+ * @copyright DeftIO 2025
6
+ *
7
+ * DO NOT EDIT - This file is vendored from the quikdown project.
8
+ * Update by copying from quikdown/dist/quikdown.esm.js
9
+ */
10
+ /**
11
+ * quikdown - A minimal markdown parser optimized for chat/LLM output
12
+ * Supports tables, code blocks, lists, and common formatting
13
+ * @param {string} markdown - The markdown source text
14
+ * @param {Object} options - Optional configuration object
15
+ * @param {Function} options.fence_plugin - Custom renderer for fenced code blocks
16
+ * (content, fence_string) => html string
17
+ * @param {boolean} options.inline_styles - If true, uses inline styles instead of classes
18
+ * @param {boolean} options.bidirectional - If true, adds data-qd attributes for source tracking
19
+ * @param {boolean} options.lazy_linefeeds - If true, single newlines become <br> tags
20
+ * @returns {string} - The rendered HTML
21
+ */
22
+
23
+ // Version will be injected at build time
24
+ const quikdownVersion = '1.2.0';
25
+
26
+ // Constants for reuse
27
+ const CLASS_PREFIX = 'quikdown-';
28
+ const PLACEHOLDER_CB = '\u00a7CB';
29
+ const PLACEHOLDER_IC = '\u00a7IC';
30
+
31
+ // Escape map at module level
32
+ const ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'};
33
+
34
+ // Single source of truth for all style definitions - optimized
35
+ const QUIKDOWN_STYLES = {
36
+ h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',
37
+ h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',
38
+ h3: 'font-size:1.25em;font-weight:600;margin:1em 0',
39
+ h4: 'font-size:1em;font-weight:600;margin:1.33em 0',
40
+ h5: 'font-size:.875em;font-weight:600;margin:1.67em 0',
41
+ h6: 'font-size:.85em;font-weight:600;margin:2em 0',
42
+ pre: 'background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0',
43
+ code: 'background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace',
44
+ blockquote: 'border-left:4px solid #ddd;margin-left:0;padding-left:1em',
45
+ table: 'border-collapse:collapse;width:100%;margin:1em 0',
46
+ th: 'border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left',
47
+ td: 'border:1px solid #ddd;padding:8px;text-align:left',
48
+ hr: 'border:none;border-top:1px solid #ddd;margin:1em 0',
49
+ img: 'max-width:100%;height:auto',
50
+ a: 'color:#06c;text-decoration:underline',
51
+ strong: 'font-weight:bold',
52
+ em: 'font-style:italic',
53
+ del: 'text-decoration:line-through',
54
+ ul: 'margin:.5em 0;padding-left:2em',
55
+ ol: 'margin:.5em 0;padding-left:2em',
56
+ li: 'margin:.25em 0',
57
+ // Task list specific styles
58
+ 'task-item': 'list-style:none',
59
+ 'task-checkbox': 'margin-right:.5em'
60
+ };
61
+
62
+ // Factory function to create getAttr for a given context
63
+ function createGetAttr(inline_styles, styles) {
64
+ return function(tag, additionalStyle = '') {
65
+ if (inline_styles) {
66
+ let style = styles[tag];
67
+ if (!style && !additionalStyle) return '';
68
+
69
+ // Remove default text-align if we're adding a different alignment
70
+ if (additionalStyle && additionalStyle.includes('text-align') && style && style.includes('text-align')) {
71
+ style = style.replace(/text-align:[^;]+;?/, '').trim();
72
+ if (style && !style.endsWith(';')) style += ';';
73
+ }
74
+
75
+ /* istanbul ignore next - defensive: additionalStyle without style doesn't occur with current tags */
76
+ const fullStyle = additionalStyle ? (style ? `${style}${additionalStyle}` : additionalStyle) : style;
77
+ return ` style="${fullStyle}"`;
78
+ } else {
79
+ const classAttr = ` class="${CLASS_PREFIX}${tag}"`;
80
+ // Apply inline styles for alignment even when using CSS classes
81
+ if (additionalStyle) {
82
+ return `${classAttr} style="${additionalStyle}"`;
83
+ }
84
+ return classAttr;
85
+ }
86
+ };
87
+ }
88
+
89
+ function quikdown(markdown, options = {}) {
90
+ if (!markdown || typeof markdown !== 'string') {
91
+ return '';
92
+ }
93
+
94
+ const { fence_plugin, inline_styles = false, bidirectional = false, lazy_linefeeds = false } = options;
95
+ const styles = QUIKDOWN_STYLES; // Use module-level styles
96
+ const getAttr = createGetAttr(inline_styles, styles); // Create getAttr once
97
+
98
+ // Escape HTML entities to prevent XSS
99
+ function escapeHtml(text) {
100
+ return text.replace(/[&<>"']/g, m => ESC_MAP[m]);
101
+ }
102
+
103
+ // Helper to add data-qd attributes for bidirectional support
104
+ const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
105
+
106
+ // Sanitize URLs to prevent XSS attacks
107
+ function sanitizeUrl(url, allowUnsafe = false) {
108
+ /* istanbul ignore next - defensive programming, regex ensures url is never empty */
109
+ if (!url) return '';
110
+
111
+ // If unsafe URLs are explicitly allowed, return as-is
112
+ if (allowUnsafe) return url;
113
+
114
+ const trimmedUrl = url.trim();
115
+ const lowerUrl = trimmedUrl.toLowerCase();
116
+
117
+ // Block dangerous protocols
118
+ const dangerousProtocols = ['javascript:', 'vbscript:', 'data:'];
119
+
120
+ for (const protocol of dangerousProtocols) {
121
+ if (lowerUrl.startsWith(protocol)) {
122
+ // Exception: Allow data:image/* for images
123
+ if (protocol === 'data:' && lowerUrl.startsWith('data:image/')) {
124
+ return trimmedUrl;
125
+ }
126
+ // Return safe empty link for dangerous protocols
127
+ return '#';
128
+ }
129
+ }
130
+
131
+ return trimmedUrl;
132
+ }
133
+
134
+ // Process the markdown in phases
135
+ let html = markdown;
136
+
137
+ // Phase 1: Extract and protect code blocks and inline code
138
+ const codeBlocks = [];
139
+ const inlineCodes = [];
140
+
141
+ // Extract fenced code blocks first (supports both ``` and ~~~)
142
+ // Match paired fences - ``` with ``` and ~~~ with ~~~
143
+ // Fence must be at start of line
144
+ html = html.replace(/^(```|~~~)([^\n]*)\n([\s\S]*?)^\1$/gm, (match, fence, lang, code) => {
145
+ const placeholder = `${PLACEHOLDER_CB}${codeBlocks.length}\u00a7`;
146
+
147
+ // Trim the language specification
148
+ const langTrimmed = lang ? lang.trim() : '';
149
+
150
+ // If custom fence plugin is provided, use it (v1.1.0: object format required)
151
+ if (fence_plugin && fence_plugin.render && typeof fence_plugin.render === 'function') {
152
+ codeBlocks.push({
153
+ lang: langTrimmed,
154
+ code: code.trimEnd(),
155
+ custom: true,
156
+ fence: fence,
157
+ hasReverse: !!fence_plugin.reverse
158
+ });
159
+ } else {
160
+ codeBlocks.push({
161
+ lang: langTrimmed,
162
+ code: escapeHtml(code.trimEnd()),
163
+ custom: false,
164
+ fence: fence
165
+ });
166
+ }
167
+ return placeholder;
168
+ });
169
+
170
+ // Extract inline code
171
+ html = html.replace(/`([^`]+)`/g, (match, code) => {
172
+ const placeholder = `${PLACEHOLDER_IC}${inlineCodes.length}\u00a7`;
173
+ inlineCodes.push(escapeHtml(code));
174
+ return placeholder;
175
+ });
176
+
177
+ // Now escape HTML in the rest of the content
178
+ html = escapeHtml(html);
179
+
180
+ // Phase 2: Process block elements
181
+
182
+ // Process tables
183
+ html = processTable(html, getAttr);
184
+
185
+ // Process headings (supports optional trailing #'s)
186
+ html = html.replace(/^(#{1,6})\s+(.+?)\s*#*$/gm, (match, hashes, content) => {
187
+ const level = hashes.length;
188
+ return `<h${level}${getAttr('h' + level)}${dataQd(hashes)}>${content}</h${level}>`;
189
+ });
190
+
191
+ // Process blockquotes (must handle escaped > since we already escaped HTML)
192
+ html = html.replace(/^&gt;\s+(.+)$/gm, `<blockquote${getAttr('blockquote')}>$1</blockquote>`);
193
+ // Merge consecutive blockquotes
194
+ html = html.replace(/<\/blockquote>\n<blockquote>/g, '\n');
195
+
196
+ // Process horizontal rules (allow trailing spaces)
197
+ html = html.replace(/^---+\s*$/gm, `<hr${getAttr('hr')}>`);
198
+
199
+ // Process lists
200
+ html = processLists(html, getAttr, inline_styles, bidirectional);
201
+
202
+ // Phase 3: Process inline elements
203
+
204
+ // Images (must come before links, with URL sanitization)
205
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
206
+ const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
207
+ const altAttr = bidirectional && alt ? ` data-qd-alt="${escapeHtml(alt)}"` : '';
208
+ const srcAttr = bidirectional ? ` data-qd-src="${escapeHtml(src)}"` : '';
209
+ return `<img${getAttr('img')} src="${sanitizedSrc}" alt="${alt}"${altAttr}${srcAttr}${dataQd('!')}>`;
210
+ });
211
+
212
+ // Links (with URL sanitization)
213
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
214
+ // Sanitize URL to prevent XSS
215
+ const sanitizedHref = sanitizeUrl(href, options.allow_unsafe_urls);
216
+ const isExternal = /^https?:\/\//i.test(sanitizedHref);
217
+ const rel = isExternal ? ' rel="noopener noreferrer"' : '';
218
+ const textAttr = bidirectional ? ` data-qd-text="${escapeHtml(text)}"` : '';
219
+ return `<a${getAttr('a')} href="${sanitizedHref}"${rel}${textAttr}${dataQd('[')}>${text}</a>`;
220
+ });
221
+
222
+ // Autolinks - convert bare URLs to clickable links
223
+ html = html.replace(/(^|\s)(https?:\/\/[^\s<]+)/g, (match, prefix, url) => {
224
+ const sanitizedUrl = sanitizeUrl(url, options.allow_unsafe_urls);
225
+ return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
226
+ });
227
+
228
+ // Process inline formatting (bold, italic, strikethrough)
229
+ const inlinePatterns = [
230
+ [/\*\*(.+?)\*\*/g, 'strong', '**'],
231
+ [/__(.+?)__/g, 'strong', '__'],
232
+ [/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, 'em', '*'],
233
+ [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em', '_'],
234
+ [/~~(.+?)~~/g, 'del', '~~']
235
+ ];
236
+
237
+ inlinePatterns.forEach(([pattern, tag, marker]) => {
238
+ html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);
239
+ });
240
+
241
+ // Line breaks
242
+ if (lazy_linefeeds) {
243
+ // Lazy linefeeds: single newline becomes <br> (except between paragraphs and after/before block elements)
244
+ const blocks = [];
245
+ let bi = 0;
246
+
247
+ // Protect tables and lists
248
+ html = html.replace(/<(table|[uo]l)[^>]*>[\s\S]*?<\/\1>/g, m => {
249
+ blocks[bi] = m;
250
+ return `\u00a7B${bi++}\u00a7`;
251
+ });
252
+
253
+ // Handle paragraphs and block elements
254
+ html = html.replace(/\n\n+/g, '\u00a7P\u00a7')
255
+ // After block elements
256
+ .replace(/(<\/(?:h[1-6]|blockquote|pre)>)\n/g, '$1\u00a7N\u00a7')
257
+ .replace(/(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)\n/g, '$1\u00a7N\u00a7')
258
+ // Before block elements
259
+ .replace(/\n(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)/g, '\u00a7N\u00a7$1')
260
+ .replace(/\n(\u00a7B\d+\u00a7)/g, '\u00a7N\u00a7$1')
261
+ .replace(/(\u00a7B\d+\u00a7)\n/g, '$1\u00a7N\u00a7')
262
+ // Convert remaining newlines
263
+ .replace(/\n/g, `<br${getAttr('br')}>`)
264
+ // Restore
265
+ .replace(/\u00a7N\u00a7/g, '\n')
266
+ .replace(/\u00a7P\u00a7/g, '</p><p>');
267
+
268
+ // Restore protected blocks
269
+ blocks.forEach((b, i) => html = html.replace(`\u00a7B${i}\u00a7`, b));
270
+
271
+ html = '<p>' + html + '</p>';
272
+ } else {
273
+ // Standard: two spaces at end of line for line breaks
274
+ html = html.replace(/ $/gm, `<br${getAttr('br')}>`);
275
+
276
+ // Paragraphs (double newlines)
277
+ // Don't add </p> after block elements (they're not in paragraphs)
278
+ html = html.replace(/\n\n+/g, (match, offset) => {
279
+ // Check if we're after a block element closing tag
280
+ const before = html.substring(0, offset);
281
+ if (before.match(/<\/(h[1-6]|blockquote|ul|ol|table|pre|hr)>$/)) {
282
+ return '<p>'; // Just open a new paragraph
283
+ }
284
+ return '</p><p>'; // Normal paragraph break
285
+ });
286
+ html = '<p>' + html + '</p>';
287
+ }
288
+
289
+ // Clean up empty paragraphs and unwrap block elements
290
+ const cleanupPatterns = [
291
+ [/<p><\/p>/g, ''],
292
+ [/<p>(<h[1-6][^>]*>)/g, '$1'],
293
+ [/(<\/h[1-6]>)<\/p>/g, '$1'],
294
+ [/<p>(<blockquote[^>]*>)/g, '$1'],
295
+ [/(<\/blockquote>)<\/p>/g, '$1'],
296
+ [/<p>(<ul[^>]*>|<ol[^>]*>)/g, '$1'],
297
+ [/(<\/ul>|<\/ol>)<\/p>/g, '$1'],
298
+ [/<p>(<hr[^>]*>)<\/p>/g, '$1'],
299
+ [/<p>(<table[^>]*>)/g, '$1'],
300
+ [/(<\/table>)<\/p>/g, '$1'],
301
+ [/<p>(<pre[^>]*>)/g, '$1'],
302
+ [/(<\/pre>)<\/p>/g, '$1'],
303
+ [new RegExp(`<p>(${PLACEHOLDER_CB}\\d+\u00a7)<\/p>`, 'g'), '$1']
304
+ ];
305
+
306
+ cleanupPatterns.forEach(([pattern, replacement]) => {
307
+ html = html.replace(pattern, replacement);
308
+ });
309
+
310
+ // Fix orphaned closing </p> tags after block elements
311
+ // When a paragraph follows a block element, ensure it has opening <p>
312
+ html = html.replace(/(<\/(?:h[1-6]|blockquote|ul|ol|table|pre|hr)>)\n([^<])/g, '$1\n<p>$2');
313
+
314
+ // Phase 4: Restore code blocks and inline code
315
+
316
+ // Restore code blocks
317
+ codeBlocks.forEach((block, i) => {
318
+ let replacement;
319
+
320
+ if (block.custom && fence_plugin && fence_plugin.render) {
321
+ // Use custom fence plugin (v1.1.0: object format with render function)
322
+ replacement = fence_plugin.render(block.code, block.lang);
323
+
324
+ // If plugin returns undefined, fall back to default rendering
325
+ if (replacement === undefined) {
326
+ const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
327
+ const codeAttr = inline_styles ? getAttr('code') : langClass;
328
+ const langAttr = bidirectional && block.lang ? ` data-qd-lang="${escapeHtml(block.lang)}"` : '';
329
+ const fenceAttr = bidirectional ? ` data-qd-fence="${escapeHtml(block.fence)}"` : '';
330
+ replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${escapeHtml(block.code)}</code></pre>`;
331
+ } else if (bidirectional) {
332
+ // If bidirectional and plugin provided HTML, add data attributes for roundtrip
333
+ replacement = replacement.replace(/^<(\w+)/,
334
+ `<$1 data-qd-fence="${escapeHtml(block.fence)}" data-qd-lang="${escapeHtml(block.lang)}" data-qd-source="${escapeHtml(block.code)}"`);
335
+ }
336
+ } else {
337
+ // Default rendering
338
+ const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
339
+ const codeAttr = inline_styles ? getAttr('code') : langClass;
340
+ const langAttr = bidirectional && block.lang ? ` data-qd-lang="${escapeHtml(block.lang)}"` : '';
341
+ const fenceAttr = bidirectional ? ` data-qd-fence="${escapeHtml(block.fence)}"` : '';
342
+ replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${block.code}</code></pre>`;
343
+ }
344
+
345
+ const placeholder = `${PLACEHOLDER_CB}${i}\u00a7`;
346
+ html = html.replace(placeholder, replacement);
347
+ });
348
+
349
+ // Restore inline code
350
+ inlineCodes.forEach((code, i) => {
351
+ const placeholder = `${PLACEHOLDER_IC}${i}\u00a7`;
352
+ html = html.replace(placeholder, `<code${getAttr('code')}${dataQd('`')}>${code}</code>`);
353
+ });
354
+
355
+ return html.trim();
356
+ }
357
+
358
+ /**
359
+ * Process inline markdown formatting
360
+ */
361
+ function processInlineMarkdown(text, getAttr) {
362
+
363
+ // Process inline formatting patterns
364
+ const patterns = [
365
+ [/\*\*(.+?)\*\*/g, 'strong'],
366
+ [/__(.+?)__/g, 'strong'],
367
+ [/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, 'em'],
368
+ [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em'],
369
+ [/~~(.+?)~~/g, 'del'],
370
+ [/`([^`]+)`/g, 'code']
371
+ ];
372
+
373
+ patterns.forEach(([pattern, tag]) => {
374
+ text = text.replace(pattern, `<${tag}${getAttr(tag)}>$1</${tag}>`);
375
+ });
376
+
377
+ return text;
378
+ }
379
+
380
+ /**
381
+ * Process markdown tables
382
+ */
383
+ function processTable(text, getAttr) {
384
+ const lines = text.split('\n');
385
+ const result = [];
386
+ let inTable = false;
387
+ let tableLines = [];
388
+
389
+ for (let i = 0; i < lines.length; i++) {
390
+ const line = lines[i].trim();
391
+
392
+ // Check if this line looks like a table row (with or without trailing |)
393
+ if (line.includes('|') && (line.startsWith('|') || /[^\\|]/.test(line))) {
394
+ if (!inTable) {
395
+ inTable = true;
396
+ tableLines = [];
397
+ }
398
+ tableLines.push(line);
399
+ } else {
400
+ // Not a table line
401
+ if (inTable) {
402
+ // Process the accumulated table
403
+ const tableHtml = buildTable(tableLines, getAttr);
404
+ if (tableHtml) {
405
+ result.push(tableHtml);
406
+ } else {
407
+ // Not a valid table, restore original lines
408
+ result.push(...tableLines);
409
+ }
410
+ inTable = false;
411
+ tableLines = [];
412
+ }
413
+ result.push(lines[i]);
414
+ }
415
+ }
416
+
417
+ // Handle table at end of text
418
+ if (inTable && tableLines.length > 0) {
419
+ const tableHtml = buildTable(tableLines, getAttr);
420
+ if (tableHtml) {
421
+ result.push(tableHtml);
422
+ } else {
423
+ result.push(...tableLines);
424
+ }
425
+ }
426
+
427
+ return result.join('\n');
428
+ }
429
+
430
+ /**
431
+ * Build an HTML table from markdown table lines
432
+ */
433
+ function buildTable(lines, getAttr) {
434
+
435
+ if (lines.length < 2) return null;
436
+
437
+ // Check for separator line (second line should be the separator)
438
+ let separatorIndex = -1;
439
+ for (let i = 1; i < lines.length; i++) {
440
+ // Support separator with or without leading/trailing pipes
441
+ if (/^\|?[\s\-:|]+\|?$/.test(lines[i]) && lines[i].includes('-')) {
442
+ separatorIndex = i;
443
+ break;
444
+ }
445
+ }
446
+
447
+ if (separatorIndex === -1) return null;
448
+
449
+ const headerLines = lines.slice(0, separatorIndex);
450
+ const bodyLines = lines.slice(separatorIndex + 1);
451
+
452
+ // Parse alignment from separator
453
+ const separator = lines[separatorIndex];
454
+ // Handle pipes at start/end or not
455
+ const separatorCells = separator.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
456
+ const alignments = separatorCells.map(cell => {
457
+ const trimmed = cell.trim();
458
+ if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';
459
+ if (trimmed.endsWith(':')) return 'right';
460
+ return 'left';
461
+ });
462
+
463
+ let html = `<table${getAttr('table')}>\n`;
464
+
465
+ // Build header
466
+ // Note: headerLines will always have length > 0 since separatorIndex starts from 1
467
+ html += `<thead${getAttr('thead')}>\n`;
468
+ headerLines.forEach(line => {
469
+ html += `<tr${getAttr('tr')}>\n`;
470
+ // Handle pipes at start/end or not
471
+ const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
472
+ cells.forEach((cell, i) => {
473
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
474
+ const processedCell = processInlineMarkdown(cell.trim(), getAttr);
475
+ html += `<th${getAttr('th', alignStyle)}>${processedCell}</th>\n`;
476
+ });
477
+ html += '</tr>\n';
478
+ });
479
+ html += '</thead>\n';
480
+
481
+ // Build body
482
+ if (bodyLines.length > 0) {
483
+ html += `<tbody${getAttr('tbody')}>\n`;
484
+ bodyLines.forEach(line => {
485
+ html += `<tr${getAttr('tr')}>\n`;
486
+ // Handle pipes at start/end or not
487
+ const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
488
+ cells.forEach((cell, i) => {
489
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
490
+ const processedCell = processInlineMarkdown(cell.trim(), getAttr);
491
+ html += `<td${getAttr('td', alignStyle)}>${processedCell}</td>\n`;
492
+ });
493
+ html += '</tr>\n';
494
+ });
495
+ html += '</tbody>\n';
496
+ }
497
+
498
+ html += '</table>';
499
+ return html;
500
+ }
501
+
502
+ /**
503
+ * Process markdown lists (ordered and unordered)
504
+ */
505
+ function processLists(text, getAttr, inline_styles, bidirectional) {
506
+
507
+ const lines = text.split('\n');
508
+ const result = [];
509
+ let listStack = []; // Track nested lists
510
+
511
+ // Helper to escape HTML for data-qd attributes
512
+ const escapeHtml = (text) => text.replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[m]);
513
+ const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
514
+
515
+ for (let i = 0; i < lines.length; i++) {
516
+ const line = lines[i];
517
+ const match = line.match(/^(\s*)([*\-+]|\d+\.)\s+(.+)$/);
518
+
519
+ if (match) {
520
+ const [, indent, marker, content] = match;
521
+ const level = Math.floor(indent.length / 2);
522
+ const isOrdered = /^\d+\./.test(marker);
523
+ const listType = isOrdered ? 'ol' : 'ul';
524
+
525
+ // Check for task list items
526
+ let listItemContent = content;
527
+ let taskListClass = '';
528
+ const taskMatch = content.match(/^\[([x ])\]\s+(.*)$/i);
529
+ if (taskMatch && !isOrdered) {
530
+ const [, checked, taskContent] = taskMatch;
531
+ const isChecked = checked.toLowerCase() === 'x';
532
+ const checkboxAttr = inline_styles
533
+ ? ' style="margin-right:.5em"'
534
+ : ` class="${CLASS_PREFIX}task-checkbox"`;
535
+ listItemContent = `<input type="checkbox"${checkboxAttr}${isChecked ? ' checked' : ''} disabled> ${taskContent}`;
536
+ taskListClass = inline_styles ? ' style="list-style:none"' : ` class="${CLASS_PREFIX}task-item"`;
537
+ }
538
+
539
+ // Close deeper levels
540
+ while (listStack.length > level + 1) {
541
+ const list = listStack.pop();
542
+ result.push(`</${list.type}>`);
543
+ }
544
+
545
+ // Open new level if needed
546
+ if (listStack.length === level) {
547
+ // Need to open a new list
548
+ listStack.push({ type: listType, level });
549
+ result.push(`<${listType}${getAttr(listType)}>`);
550
+ } else if (listStack.length === level + 1) {
551
+ // Check if we need to switch list type
552
+ const currentList = listStack[listStack.length - 1];
553
+ if (currentList.type !== listType) {
554
+ result.push(`</${currentList.type}>`);
555
+ listStack.pop();
556
+ listStack.push({ type: listType, level });
557
+ result.push(`<${listType}${getAttr(listType)}>`);
558
+ }
559
+ }
560
+
561
+ const liAttr = taskListClass || getAttr('li');
562
+ result.push(`<li${liAttr}${dataQd(marker)}>${listItemContent}</li>`);
563
+ } else {
564
+ // Not a list item, close all lists
565
+ while (listStack.length > 0) {
566
+ const list = listStack.pop();
567
+ result.push(`</${list.type}>`);
568
+ }
569
+ result.push(line);
570
+ }
571
+ }
572
+
573
+ // Close any remaining lists
574
+ while (listStack.length > 0) {
575
+ const list = listStack.pop();
576
+ result.push(`</${list.type}>`);
577
+ }
578
+
579
+ return result.join('\n');
580
+ }
581
+
582
+ /**
583
+ * Emit CSS styles for quikdown elements
584
+ * @param {string} prefix - Optional class prefix (default: 'quikdown-')
585
+ * @param {string} theme - Optional theme: 'light' (default) or 'dark'
586
+ * @returns {string} CSS string with quikdown styles
587
+ */
588
+ quikdown.emitStyles = function(prefix = 'quikdown-', theme = 'light') {
589
+ const styles = QUIKDOWN_STYLES;
590
+
591
+ // Define theme color overrides
592
+ const themeOverrides = {
593
+ dark: {
594
+ '#f4f4f4': '#2a2a2a', // pre background
595
+ '#f0f0f0': '#2a2a2a', // code background
596
+ '#f2f2f2': '#2a2a2a', // th background
597
+ '#ddd': '#3a3a3a', // borders
598
+ '#06c': '#6db3f2', // links
599
+ _textColor: '#e0e0e0'
600
+ },
601
+ light: {
602
+ _textColor: '#333' // Explicit text color for light theme
603
+ }
604
+ };
605
+
606
+ let css = '';
607
+ for (const [tag, style] of Object.entries(styles)) {
608
+ let themedStyle = style;
609
+
610
+ // Apply theme overrides if dark theme
611
+ if (theme === 'dark' && themeOverrides.dark) {
612
+ // Replace colors
613
+ for (const [oldColor, newColor] of Object.entries(themeOverrides.dark)) {
614
+ if (!oldColor.startsWith('_')) {
615
+ themedStyle = themedStyle.replace(new RegExp(oldColor, 'g'), newColor);
616
+ }
617
+ }
618
+
619
+ // Add text color for certain elements in dark theme
620
+ const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
621
+ if (needsTextColor.includes(tag)) {
622
+ themedStyle += `;color:${themeOverrides.dark._textColor}`;
623
+ }
624
+ } else if (theme === 'light' && themeOverrides.light) {
625
+ // Add explicit text color for light theme elements too
626
+ const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
627
+ if (needsTextColor.includes(tag)) {
628
+ themedStyle += `;color:${themeOverrides.light._textColor}`;
629
+ }
630
+ }
631
+
632
+ css += `.${prefix}${tag} { ${themedStyle} }\n`;
633
+ }
634
+
635
+ return css;
636
+ };
637
+
638
+ /**
639
+ * Configure quikdown with options and return a function
640
+ * @param {Object} options - Configuration options
641
+ * @returns {Function} Configured quikdown function
642
+ */
643
+ quikdown.configure = function(options) {
644
+ return function(markdown) {
645
+ return quikdown(markdown, options);
646
+ };
647
+ };
648
+
649
+ /**
650
+ * Version information
651
+ */
652
+ quikdown.version = quikdownVersion;
653
+
654
+ export { quikdown as default };
package/src/version.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Auto-generated version file from package.json
3
+ * DO NOT EDIT DIRECTLY - Use npm run generate-version
4
+ */
5
+
6
+ export const VERSION = '2.0.7';
7
+ export const VERSION_INFO = {
8
+ version: '2.0.7',
9
+ name: 'bitwrench',
10
+ description: 'A library for javascript UI functions.',
11
+ license: 'BSD-2-Clause',
12
+ homepage: 'http://deftio.com/bitwrench',
13
+ repository: 'git+https://github.com/deftio/bitwrench.git',
14
+ author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
15
+ buildDate: '2026-03-06T09:27:27.033Z'
16
+ };