flexdesk 0.16.1 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/LICENSE +0 -0
- package/README.md +2435 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/canvas.js +18 -0
- package/charts.js +21 -0
- package/core.js +14 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3165 -159
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +707 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
- package/dist/chunk-LWV23YVE.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
- package/dist/chunk-RMTRWWEO.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3165 -159
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3290 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/editor.js +20 -0
- package/flow.js +21 -0
- package/host.js +14 -0
- package/package.json +12 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +293 -0
- package/src/ui/components/cell_editors.js +480 -0
- package/src/ui/components/cell_grid.js +761 -0
- package/src/ui/components/cell_paste.js +243 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +696 -71
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +361 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/tsv.js +74 -0
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/tiles.js +27 -0
- package/widgets.js +69 -0
- package/wm.js +28 -0
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-B5Q53LAJ.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- package/src/ui/controllers/panel_state_machine.js +0 -634
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderMarkdown — markdown to an HTML string, safe by default.
|
|
3
|
+
*
|
|
4
|
+
* A CommonMark-ish subset: ATX headings, paragraphs, emphasis, strong,
|
|
5
|
+
* strikethrough, code spans, fenced code, block quotes, ordered and unordered
|
|
6
|
+
* lists (nested), GFM pipe tables, thematic breaks, links, images, autolinks,
|
|
7
|
+
* hard line breaks, backslash escapes and `$…$` / `$$…$$` math. Not in it:
|
|
8
|
+
* setext headings, indented code blocks, reference links, raw HTML blocks.
|
|
9
|
+
*
|
|
10
|
+
* SAFE BY DEFAULT. Every character of the source is text unless markdown makes
|
|
11
|
+
* it markup:
|
|
12
|
+
*
|
|
13
|
+
* - raw HTML is escaped. `options.allowHtml` names tags that may pass
|
|
14
|
+
* through — the tag only, never its attributes, and never a tag that runs
|
|
15
|
+
* or embeds anything (`script`, `iframe`, `svg`, … throw). It is empty by
|
|
16
|
+
* default. An allowed tag left open is closed at the end of its paragraph,
|
|
17
|
+
* and a closer with no opener is shown as text.
|
|
18
|
+
* - a link goes out only to `http:`, `https:`, `mailto:` or a `#anchor`,
|
|
19
|
+
* always with `rel="noopener noreferrer"`. Any other target — a
|
|
20
|
+
* `javascript:` URL, a `data:` URL, a relative path — renders the link's
|
|
21
|
+
* text and no anchor. An image loads only from `http(s):` or a relative
|
|
22
|
+
* path; otherwise its alt text is shown.
|
|
23
|
+
*
|
|
24
|
+
* What the CALLER returns is trusted: the highlighter's HTML, the math hook's
|
|
25
|
+
* HTML and the extension rules' HTML go into the output as they are.
|
|
26
|
+
*
|
|
27
|
+
* WELL-FORMED WHILE STREAMING. Every element this renderer opens it closes,
|
|
28
|
+
* so any prefix of a document — a chat reply arriving token by token — renders
|
|
29
|
+
* to balanced HTML: an open code fence is closed at the end of the input, an
|
|
30
|
+
* emphasis or a link whose closer has not arrived yet is text, and a table
|
|
31
|
+
* whose delimiter row has not arrived yet is a paragraph.
|
|
32
|
+
*
|
|
33
|
+
* renderMarkdown(source, {
|
|
34
|
+
* inline: false, // true: inline markup only, no blocks, no <p>
|
|
35
|
+
* allowHtml: [], // e.g. ['kbd', 'br']
|
|
36
|
+
* highlight: null, // (code, lang) => html | null (null: escaped)
|
|
37
|
+
* math: undefined, // undefined: window.katex when present;
|
|
38
|
+
* // false: `$` is text; or (tex, {display}) => html | null
|
|
39
|
+
* inlineRules: [], // [{ pattern: /\[\[ref:(\w+)\]\]/, render: (match, ctx) => html | null }]
|
|
40
|
+
* blockRules: [], // [{ start: /^:::note/, end: /^:::$/, render: ({lines, match, closed}, ctx) => html | null }]
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* A rule whose render answers null or undefined declines, and the text is
|
|
44
|
+
* read as markdown. `ctx` carries `escape(text)` and `inline(text)` (render
|
|
45
|
+
* markdown inline with the same options). An inline rule is tried at every
|
|
46
|
+
* position outside a code span, before the built-in markup; a block rule's
|
|
47
|
+
* `start` is tested against each line that begins a block, and the block runs
|
|
48
|
+
* to the first later line matching `end` (inclusive) — or, with no `end`, is
|
|
49
|
+
* that one line. A block whose `end` has not arrived is passed with
|
|
50
|
+
* `closed: false` and runs to the end of the input.
|
|
51
|
+
*
|
|
52
|
+
* @module markdown
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/** Tags `allowHtml` refuses: each runs script, embeds content or takes input. */
|
|
56
|
+
const NEVER_ALLOWED = new Set([
|
|
57
|
+
'script', 'style', 'iframe', 'frame', 'frameset', 'object', 'embed', 'applet',
|
|
58
|
+
'template', 'noscript', 'textarea', 'title', 'xmp', 'plaintext', 'noembed',
|
|
59
|
+
'svg', 'math', 'link', 'meta', 'base', 'form', 'input', 'button', 'select',
|
|
60
|
+
'option', 'video', 'audio', 'source', 'track', 'img', 'image', 'html',
|
|
61
|
+
'head', 'body', 'portal', 'dialog',
|
|
62
|
+
]);
|
|
63
|
+
const VOID_TAGS = new Set(['br', 'hr', 'wbr']);
|
|
64
|
+
|
|
65
|
+
const LINK_URL = /^(?:https?:\/\/|mailto:|#)/i;
|
|
66
|
+
const IMAGE_URL = /^(?:https?:\/\/|\/\/|[^:/?#]*(?:[/?#]|$))/i;
|
|
67
|
+
const ENTITY = /^&(?:[a-zA-Z][a-zA-Z0-9]{1,31}|#\d{1,7}|#[xX][0-9a-fA-F]{1,6});/;
|
|
68
|
+
const ASCII_PUNCT = /[!-/:-@[-`{-~]/;
|
|
69
|
+
|
|
70
|
+
const RE = {
|
|
71
|
+
blank: /^[ \t]*$/,
|
|
72
|
+
heading: /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/,
|
|
73
|
+
hr: /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/,
|
|
74
|
+
fence: /^( {0,3})(`{3,}|~{3,})[ \t]*([^\s`]*)[^`]*$/,
|
|
75
|
+
quote: /^ {0,3}> ?/,
|
|
76
|
+
item: /^( {0,3})([-*+]|\d{1,9}[.)])(?=[ \t]|$)([ \t]*)(.*)$/,
|
|
77
|
+
mathOpen: /^ {0,3}\$\$/,
|
|
78
|
+
tableDelim: /^[ \t]*\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Text for HTML: text content and attribute values alike.
|
|
83
|
+
* @param {*} value
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function escapeHtml(value) {
|
|
87
|
+
return String(value ?? '')
|
|
88
|
+
.replace(/&/g, '&')
|
|
89
|
+
.replace(/</g, '<')
|
|
90
|
+
.replace(/>/g, '>')
|
|
91
|
+
.replace(/"/g, '"')
|
|
92
|
+
.replace(/'/g, ''');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The default math hook: window.katex, looked up per call, when it is there. */
|
|
96
|
+
function katexMath(tex, { display }) {
|
|
97
|
+
const katex = globalThis.window?.katex ?? globalThis.katex;
|
|
98
|
+
if (typeof katex?.renderToString !== 'function') return null;
|
|
99
|
+
try {
|
|
100
|
+
return katex.renderToString(tex, { displayMode: display, throwOnError: false });
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function compileOptions(options) {
|
|
107
|
+
const allow = new Set();
|
|
108
|
+
for (const raw of options.allowHtml ?? []) {
|
|
109
|
+
const tag = String(raw).toLowerCase();
|
|
110
|
+
if (!/^[a-z][a-z0-9-]*$/.test(tag)) {
|
|
111
|
+
throw new TypeError(`renderMarkdown: allowHtml entry "${raw}" is not a tag name`);
|
|
112
|
+
}
|
|
113
|
+
if (NEVER_ALLOWED.has(tag)) {
|
|
114
|
+
throw new TypeError(`renderMarkdown: <${tag}> cannot be allowed`);
|
|
115
|
+
}
|
|
116
|
+
allow.add(tag);
|
|
117
|
+
}
|
|
118
|
+
const fn = (value, name) => {
|
|
119
|
+
if (value == null) return null;
|
|
120
|
+
if (typeof value !== 'function') throw new TypeError(`renderMarkdown: ${name} must be a function`);
|
|
121
|
+
return value;
|
|
122
|
+
};
|
|
123
|
+
const inlineRules = (options.inlineRules ?? []).map((rule, i) => {
|
|
124
|
+
if (!(rule?.pattern instanceof RegExp) || typeof rule.render !== 'function') {
|
|
125
|
+
throw new TypeError(`renderMarkdown: inlineRules[${i}] needs a RegExp pattern and a render function`);
|
|
126
|
+
}
|
|
127
|
+
// Sticky: the rule matches AT the position being read, never further on.
|
|
128
|
+
const flags = rule.pattern.flags.replace(/[gy]/g, '') + 'y';
|
|
129
|
+
return { pattern: new RegExp(rule.pattern.source, flags), render: rule.render };
|
|
130
|
+
});
|
|
131
|
+
const blockRules = (options.blockRules ?? []).map((rule, i) => {
|
|
132
|
+
if (!(rule?.start instanceof RegExp) || typeof rule.render !== 'function'
|
|
133
|
+
|| (rule.end != null && !(rule.end instanceof RegExp))) {
|
|
134
|
+
throw new TypeError(`renderMarkdown: blockRules[${i}] needs a RegExp start, an optional RegExp end and a render function`);
|
|
135
|
+
}
|
|
136
|
+
const plain = (re) => new RegExp(re.source, re.flags.replace(/[gy]/g, ''));
|
|
137
|
+
return { start: plain(rule.start), end: rule.end ? plain(rule.end) : null, render: rule.render };
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
allow,
|
|
141
|
+
highlight: fn(options.highlight, 'highlight'),
|
|
142
|
+
math: options.math === undefined ? katexMath
|
|
143
|
+
: options.math === false || options.math === null ? null
|
|
144
|
+
: fn(options.math, 'math'),
|
|
145
|
+
inlineRules,
|
|
146
|
+
blockRules,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
151
|
+
// Inline
|
|
152
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
153
|
+
|
|
154
|
+
/** Index of the run of exactly `len` backticks closing a code span, or -1. */
|
|
155
|
+
function findBacktickCloser(text, from, len) {
|
|
156
|
+
let i = from;
|
|
157
|
+
while (i < text.length) {
|
|
158
|
+
const j = text.indexOf('`', i);
|
|
159
|
+
if (j === -1) return -1;
|
|
160
|
+
let k = j;
|
|
161
|
+
while (text[k] === '`') k++;
|
|
162
|
+
if (k - j === len) return j;
|
|
163
|
+
i = k;
|
|
164
|
+
}
|
|
165
|
+
return -1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Index after the `]` matching the `[` at `open`, or -1. */
|
|
169
|
+
function findBracketClose(text, open) {
|
|
170
|
+
let depth = 0;
|
|
171
|
+
for (let i = open; i < text.length; i++) {
|
|
172
|
+
const c = text[i];
|
|
173
|
+
if (c === '\\') { i++; continue; }
|
|
174
|
+
if (c === '`') {
|
|
175
|
+
let k = i;
|
|
176
|
+
while (text[k] === '`') k++;
|
|
177
|
+
const close = findBacktickCloser(text, k, k - i);
|
|
178
|
+
if (close !== -1) { i = close + (k - i) - 1; continue; }
|
|
179
|
+
i = k - 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (c === '[') depth++;
|
|
183
|
+
else if (c === ']' && --depth === 0) return i;
|
|
184
|
+
}
|
|
185
|
+
return -1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** `(url "title")` starting at `open`: {url, title, end} or null. */
|
|
189
|
+
function parseDestination(text, open) {
|
|
190
|
+
if (text[open] !== '(') return null;
|
|
191
|
+
let i = open + 1;
|
|
192
|
+
while (text[i] === ' ' || text[i] === '\t' || text[i] === '\n') i++;
|
|
193
|
+
let url = '';
|
|
194
|
+
if (text[i] === '<') {
|
|
195
|
+
const close = text.indexOf('>', i);
|
|
196
|
+
if (close === -1 || /[\n<]/.test(text.slice(i + 1, close))) return null;
|
|
197
|
+
url = text.slice(i + 1, close);
|
|
198
|
+
i = close + 1;
|
|
199
|
+
} else {
|
|
200
|
+
let depth = 0;
|
|
201
|
+
const start = i;
|
|
202
|
+
for (; i < text.length; i++) {
|
|
203
|
+
const c = text[i];
|
|
204
|
+
if (c === '\\' && i + 1 < text.length) { i++; continue; }
|
|
205
|
+
if (/\s/.test(c)) break;
|
|
206
|
+
if (c === '(') depth++;
|
|
207
|
+
else if (c === ')') { if (depth === 0) break; depth--; }
|
|
208
|
+
}
|
|
209
|
+
url = text.slice(start, i);
|
|
210
|
+
}
|
|
211
|
+
while (text[i] === ' ' || text[i] === '\t' || text[i] === '\n') i++;
|
|
212
|
+
let title = null;
|
|
213
|
+
const q = text[i];
|
|
214
|
+
if (q === '"' || q === "'" || q === '(') {
|
|
215
|
+
const closeQ = q === '(' ? ')' : q;
|
|
216
|
+
const close = text.indexOf(closeQ, i + 1);
|
|
217
|
+
if (close === -1) return null;
|
|
218
|
+
title = text.slice(i + 1, close);
|
|
219
|
+
i = close + 1;
|
|
220
|
+
while (text[i] === ' ' || text[i] === '\t' || text[i] === '\n') i++;
|
|
221
|
+
}
|
|
222
|
+
if (text[i] !== ')') return null;
|
|
223
|
+
return { url: url.replace(/\\([!-/:-@[-`{-~])/g, '$1'), title, end: i + 1 };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const isSpace = (c) => c === undefined || /\s/.test(c);
|
|
227
|
+
const isWord = (c) => c !== undefined && /[\p{L}\p{N}]/u.test(c);
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The closer of an emphasis run of `len` × `ch` opened at `from`: the index
|
|
231
|
+
* of a run of exactly `len` that is not preceded by whitespace (and, for `_`,
|
|
232
|
+
* not followed by a word character), or -1.
|
|
233
|
+
*/
|
|
234
|
+
function findEmphasisCloser(text, from, ch, len) {
|
|
235
|
+
let i = from;
|
|
236
|
+
while (i < text.length) {
|
|
237
|
+
const c = text[i];
|
|
238
|
+
if (c === '\\') { i += 2; continue; }
|
|
239
|
+
if (c === '`') {
|
|
240
|
+
let k = i;
|
|
241
|
+
while (text[k] === '`') k++;
|
|
242
|
+
const close = findBacktickCloser(text, k, k - i);
|
|
243
|
+
i = close === -1 ? k : close + (k - i);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (c !== ch) { i++; continue; }
|
|
247
|
+
let k = i;
|
|
248
|
+
while (text[k] === ch) k++;
|
|
249
|
+
if (k - i === len && i > from && !isSpace(text[i - 1])
|
|
250
|
+
&& !(ch === '_' && isWord(text[k]))) return i;
|
|
251
|
+
i = k;
|
|
252
|
+
}
|
|
253
|
+
return -1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** `$…$` or `$$…$$` at `i`: {tex, display, end} or null. */
|
|
257
|
+
function parseInlineMath(text, i) {
|
|
258
|
+
if (text[i + 1] === '$') {
|
|
259
|
+
const close = text.indexOf('$$', i + 2);
|
|
260
|
+
if (close === -1 || close === i + 2) return null;
|
|
261
|
+
return { tex: text.slice(i + 2, close), display: true, end: close + 2 };
|
|
262
|
+
}
|
|
263
|
+
if (isSpace(text[i + 1])) return null;
|
|
264
|
+
for (let j = i + 1; j < text.length; j++) {
|
|
265
|
+
if (text[j] === '\\') { j++; continue; }
|
|
266
|
+
if (text[j] === '\n' && text[j + 1] === '\n') return null;
|
|
267
|
+
if (text[j] === '$' && j > i + 1 && !isSpace(text[j - 1]) && !/\d/.test(text[j + 1] ?? '')) {
|
|
268
|
+
return { tex: text.slice(i + 1, j), display: false, end: j + 1 };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function renderInline(text, ctx, { inLink = false } = {}) {
|
|
275
|
+
const { opts } = ctx;
|
|
276
|
+
let out = '';
|
|
277
|
+
let plain = '';
|
|
278
|
+
const flush = () => { out += escapeHtml(plain); plain = ''; };
|
|
279
|
+
const emit = (html) => { flush(); out += html; };
|
|
280
|
+
const open = []; // allowed raw-HTML tags open in this span
|
|
281
|
+
|
|
282
|
+
let i = 0;
|
|
283
|
+
while (i < text.length) {
|
|
284
|
+
const c = text[i];
|
|
285
|
+
|
|
286
|
+
// Backslash: an escaped punctuation character, or a hard break.
|
|
287
|
+
if (c === '\\') {
|
|
288
|
+
const n = text[i + 1];
|
|
289
|
+
if (n === '\n') { emit('<br>\n'); i += 2; continue; }
|
|
290
|
+
if (n !== undefined && ASCII_PUNCT.test(n)) { plain += n; i += 2; continue; }
|
|
291
|
+
plain += c; i++; continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Code span: wins over everything, including the caller's rules.
|
|
295
|
+
if (c === '`') {
|
|
296
|
+
let k = i;
|
|
297
|
+
while (text[k] === '`') k++;
|
|
298
|
+
const close = findBacktickCloser(text, k, k - i);
|
|
299
|
+
if (close === -1) { plain += text.slice(i, k); i = k; continue; }
|
|
300
|
+
let code = text.slice(k, close).replace(/\n/g, ' ');
|
|
301
|
+
if (code.length > 2 && code[0] === ' ' && code.at(-1) === ' ' && code.trim()) code = code.slice(1, -1);
|
|
302
|
+
emit(`<code>${escapeHtml(code)}</code>`);
|
|
303
|
+
i = close + (k - i);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// The caller's rules.
|
|
308
|
+
let ruled = false;
|
|
309
|
+
for (const rule of opts.inlineRules) {
|
|
310
|
+
rule.pattern.lastIndex = i;
|
|
311
|
+
const m = rule.pattern.exec(text);
|
|
312
|
+
if (!m || !m[0].length) continue;
|
|
313
|
+
const html = rule.render(m, ctx.api);
|
|
314
|
+
if (html == null) continue;
|
|
315
|
+
emit(String(html));
|
|
316
|
+
i += m[0].length;
|
|
317
|
+
ruled = true;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
if (ruled) continue;
|
|
321
|
+
|
|
322
|
+
// Hard break: two or more spaces before a newline.
|
|
323
|
+
if (c === '\n') {
|
|
324
|
+
if (/ {2,}$/.test(plain)) { plain = plain.replace(/ +$/, ''); emit('<br>\n'); }
|
|
325
|
+
else { plain = plain.replace(/ +$/, ''); plain += '\n'; }
|
|
326
|
+
i++;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (c === '&') {
|
|
331
|
+
const m = ENTITY.exec(text.slice(i, i + 40));
|
|
332
|
+
if (m) { emit(m[0]); i += m[0].length; continue; }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (c === '$' && opts.math) {
|
|
336
|
+
const m = parseInlineMath(text, i);
|
|
337
|
+
if (m) {
|
|
338
|
+
const html = opts.math(m.tex, { display: m.display });
|
|
339
|
+
if (html == null) plain += text.slice(i, m.end);
|
|
340
|
+
else emit(String(html));
|
|
341
|
+
i = m.end;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Images and links.
|
|
347
|
+
if ((c === '!' && text[i + 1] === '[') || (c === '[' && !inLink)) {
|
|
348
|
+
const bracket = c === '!' ? i + 1 : i;
|
|
349
|
+
const close = findBracketClose(text, bracket);
|
|
350
|
+
const dest = close === -1 ? null : parseDestination(text, close + 1);
|
|
351
|
+
if (dest) {
|
|
352
|
+
const label = text.slice(bracket + 1, close);
|
|
353
|
+
const title = dest.title == null ? '' : ` title="${escapeHtml(dest.title)}"`;
|
|
354
|
+
if (c === '!') {
|
|
355
|
+
if (IMAGE_URL.test(dest.url) && !/[\s\x00-\x1f]/.test(dest.url)) {
|
|
356
|
+
emit(`<img src="${escapeHtml(dest.url)}" alt="${escapeHtml(label)}"${title}>`);
|
|
357
|
+
} else {
|
|
358
|
+
plain += label;
|
|
359
|
+
}
|
|
360
|
+
} else if (LINK_URL.test(dest.url) && !/[\s\x00-\x1f]/.test(dest.url)) {
|
|
361
|
+
emit(`<a href="${escapeHtml(dest.url)}"${title} rel="noopener noreferrer">`
|
|
362
|
+
+ `${renderInline(label, ctx, { inLink: true })}</a>`);
|
|
363
|
+
} else {
|
|
364
|
+
// Inert: the text the reader was shown, and nowhere to go.
|
|
365
|
+
emit(renderInline(label, ctx, { inLink: true }));
|
|
366
|
+
}
|
|
367
|
+
i = dest.end;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (c === '<') {
|
|
373
|
+
const auto = /^<((?:https?:\/\/|mailto:)[^\s<>]+)>/i.exec(text.slice(i, i + 2048));
|
|
374
|
+
if (auto && !inLink) {
|
|
375
|
+
emit(`<a href="${escapeHtml(auto[1])}" rel="noopener noreferrer">${escapeHtml(auto[1])}</a>`);
|
|
376
|
+
i += auto[0].length;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const tag = opts.allow.size ? /^<(\/?)([A-Za-z][A-Za-z0-9-]*)(?:\s[^<>]*)?\/?>/.exec(text.slice(i, i + 1024)) : null;
|
|
380
|
+
const name = tag?.[2].toLowerCase();
|
|
381
|
+
if (tag && opts.allow.has(name)) {
|
|
382
|
+
if (VOID_TAGS.has(name)) {
|
|
383
|
+
if (!tag[1]) emit(`<${name}>`);
|
|
384
|
+
i += tag[0].length;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (!tag[1]) {
|
|
388
|
+
emit(`<${name}>`);
|
|
389
|
+
open.push(name);
|
|
390
|
+
i += tag[0].length;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const at = open.lastIndexOf(name);
|
|
394
|
+
if (at !== -1) {
|
|
395
|
+
while (open.length > at) emit(`</${open.pop()}>`);
|
|
396
|
+
i += tag[0].length;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Emphasis, strong, strikethrough.
|
|
403
|
+
if (c === '*' || c === '_' || c === '~') {
|
|
404
|
+
let k = i;
|
|
405
|
+
while (text[k] === c) k++;
|
|
406
|
+
const run = k - i;
|
|
407
|
+
const opens = !isSpace(text[k]) && !(c === '_' && isWord(text[i - 1]));
|
|
408
|
+
const len = c === '~' ? (run === 2 ? 2 : 0) : run <= 3 ? run : 0;
|
|
409
|
+
if (opens && len) {
|
|
410
|
+
const close = findEmphasisCloser(text, k, c, len);
|
|
411
|
+
if (close !== -1) {
|
|
412
|
+
const inner = renderInline(text.slice(k, close), ctx, { inLink });
|
|
413
|
+
const wrapped = c === '~' ? `<del>${inner}</del>`
|
|
414
|
+
: len === 1 ? `<em>${inner}</em>`
|
|
415
|
+
: len === 2 ? `<strong>${inner}</strong>`
|
|
416
|
+
: `<strong><em>${inner}</em></strong>`;
|
|
417
|
+
emit(wrapped);
|
|
418
|
+
i = close + len;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
plain += text.slice(i, k);
|
|
423
|
+
i = k;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
plain += c;
|
|
428
|
+
i++;
|
|
429
|
+
}
|
|
430
|
+
flush();
|
|
431
|
+
while (open.length) out += `</${open.pop()}>`;
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
436
|
+
// Blocks
|
|
437
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
438
|
+
|
|
439
|
+
/** A pipe-table row's cells: unescaped `|` splits, outside code spans. */
|
|
440
|
+
function splitRow(line) {
|
|
441
|
+
let s = line.trim();
|
|
442
|
+
if (s.startsWith('|')) s = s.slice(1);
|
|
443
|
+
if (s.endsWith('|') && !s.endsWith('\\|')) s = s.slice(0, -1);
|
|
444
|
+
const cells = [];
|
|
445
|
+
let cell = '';
|
|
446
|
+
let inCode = 0;
|
|
447
|
+
for (let i = 0; i < s.length; i++) {
|
|
448
|
+
const c = s[i];
|
|
449
|
+
if (c === '\\' && s[i + 1] === '|') { cell += '|'; i++; continue; }
|
|
450
|
+
if (c === '`') {
|
|
451
|
+
let k = i;
|
|
452
|
+
while (s[k] === '`') k++;
|
|
453
|
+
const len = k - i;
|
|
454
|
+
if (inCode === len) inCode = 0;
|
|
455
|
+
else if (!inCode && findBacktickCloser(s, k, len) !== -1) inCode = len;
|
|
456
|
+
cell += s.slice(i, k);
|
|
457
|
+
i = k - 1;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (c === '|' && !inCode) { cells.push(cell.trim()); cell = ''; continue; }
|
|
461
|
+
cell += c;
|
|
462
|
+
}
|
|
463
|
+
cells.push(cell.trim());
|
|
464
|
+
return cells;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isTableStart(lines, i) {
|
|
468
|
+
if (!lines[i].includes('|') || i + 1 >= lines.length
|
|
469
|
+
|| !lines[i + 1].includes('|') || !RE.tableDelim.test(lines[i + 1])) return false;
|
|
470
|
+
return splitRow(lines[i]).length === splitRow(lines[i + 1]).length;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function blockRuleAt(line, ctx) {
|
|
474
|
+
for (const rule of ctx.opts.blockRules) {
|
|
475
|
+
const match = rule.start.exec(line);
|
|
476
|
+
if (match) return { rule, match };
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* A `$$` line opens a display-math block when the rest of it is empty or the
|
|
483
|
+
* whole formula (`$$x$$`); `$$x$$ and more` is a paragraph with inline math.
|
|
484
|
+
*/
|
|
485
|
+
function isMathBlockStart(line, ctx) {
|
|
486
|
+
if (!ctx.opts.math || !RE.mathOpen.test(line)) return false;
|
|
487
|
+
const rest = line.replace(RE.mathOpen, '');
|
|
488
|
+
return !rest.includes('$$') || /^[^$]*\$\$[ \t]*$/.test(rest) && !/^[ \t]*\$\$/.test(rest);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Does `lines[i]` begin a block that interrupts a paragraph? */
|
|
492
|
+
function startsBlock(lines, i, ctx) {
|
|
493
|
+
const line = lines[i];
|
|
494
|
+
if (RE.heading.test(line) || RE.hr.test(line) || RE.fence.test(line) || RE.quote.test(line)) return true;
|
|
495
|
+
if (isMathBlockStart(line, ctx)) return true;
|
|
496
|
+
const item = RE.item.exec(line);
|
|
497
|
+
if (item && item[4].trim() && (!/\d/.test(item[2]) || /^1[.)]$/.test(item[2]))) return true;
|
|
498
|
+
if (blockRuleAt(line, ctx)) return true;
|
|
499
|
+
return isTableStart(lines, i);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function renderCode(lines, i, ctx) {
|
|
503
|
+
const [, indent, fence, lang] = RE.fence.exec(lines[i]);
|
|
504
|
+
const body = [];
|
|
505
|
+
const close = new RegExp(`^ {0,3}${fence[0] === '`' ? '`' : '~'}{${fence.length},}[ \\t]*$`);
|
|
506
|
+
let j = i + 1;
|
|
507
|
+
for (; j < lines.length; j++) {
|
|
508
|
+
if (close.test(lines[j])) { j++; break; }
|
|
509
|
+
let line = lines[j];
|
|
510
|
+
for (let n = 0; n < indent.length && line[0] === ' '; n++) line = line.slice(1);
|
|
511
|
+
body.push(line);
|
|
512
|
+
}
|
|
513
|
+
const code = body.length ? body.join('\n') + '\n' : '';
|
|
514
|
+
const highlighted = ctx.opts.highlight ? ctx.opts.highlight(code, lang) : null;
|
|
515
|
+
const cls = lang ? ` class="language-${escapeHtml(lang)}"` : '';
|
|
516
|
+
return { html: `<pre><code${cls}>${highlighted == null ? escapeHtml(code) : String(highlighted)}</code></pre>\n`, next: j };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function renderMathBlock(lines, i, ctx) {
|
|
520
|
+
const first = lines[i].replace(/^ {0,3}\$\$/, '');
|
|
521
|
+
const body = [];
|
|
522
|
+
let j = i;
|
|
523
|
+
let closed = false;
|
|
524
|
+
if (/\$\$[ \t]*$/.test(first)) {
|
|
525
|
+
body.push(first.replace(/\$\$[ \t]*$/, ''));
|
|
526
|
+
closed = true;
|
|
527
|
+
j = i + 1;
|
|
528
|
+
} else {
|
|
529
|
+
if (first.trim()) body.push(first);
|
|
530
|
+
for (j = i + 1; j < lines.length; j++) {
|
|
531
|
+
if (/\$\$[ \t]*$/.test(lines[j])) {
|
|
532
|
+
const last = lines[j].replace(/\$\$[ \t]*$/, '');
|
|
533
|
+
if (last.trim()) body.push(last);
|
|
534
|
+
closed = true;
|
|
535
|
+
j++;
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
body.push(lines[j]);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const tex = body.join('\n').trim();
|
|
542
|
+
const html = closed && tex ? ctx.opts.math(tex, { display: true }) : null;
|
|
543
|
+
if (html != null) return { html: `<div class="twm-md-math">${String(html)}</div>\n`, next: j };
|
|
544
|
+
return { html: `<p>${escapeHtml(lines.slice(i, j).join('\n'))}</p>\n`, next: j };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function renderTable(lines, i, ctx) {
|
|
548
|
+
const head = splitRow(lines[i]);
|
|
549
|
+
const align = splitRow(lines[i + 1]).map((d) => {
|
|
550
|
+
const l = d.startsWith(':');
|
|
551
|
+
const r = d.endsWith(':');
|
|
552
|
+
return l && r ? 'center' : r ? 'right' : l ? 'left' : null;
|
|
553
|
+
});
|
|
554
|
+
const cell = (tag, text, n) => {
|
|
555
|
+
const style = align[n] ? ` style="text-align: ${align[n]}"` : '';
|
|
556
|
+
return `<${tag}${style}>${renderInline(text ?? '', ctx)}</${tag}>`;
|
|
557
|
+
};
|
|
558
|
+
let html = '<table>\n<thead>\n<tr>' + head.map((t, n) => cell('th', t, n)).join('') + '</tr>\n</thead>\n';
|
|
559
|
+
let j = i + 2;
|
|
560
|
+
const rows = [];
|
|
561
|
+
for (; j < lines.length; j++) {
|
|
562
|
+
if (RE.blank.test(lines[j]) || startsBlock(lines, j, ctx)) break;
|
|
563
|
+
const cells = splitRow(lines[j]);
|
|
564
|
+
rows.push('<tr>' + head.map((_, n) => cell('td', cells[n], n)).join('') + '</tr>\n');
|
|
565
|
+
}
|
|
566
|
+
if (rows.length) html += '<tbody>\n' + rows.join('') + '</tbody>\n';
|
|
567
|
+
return { html: html + '</table>\n', next: j };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function renderList(lines, i, ctx) {
|
|
571
|
+
const first = RE.item.exec(lines[i]);
|
|
572
|
+
const ordered = /\d/.test(first[2]);
|
|
573
|
+
const delim = ordered ? first[2].at(-1) : first[2];
|
|
574
|
+
const sameList = (m) => m && (ordered ? /\d/.test(m[2]) && m[2].at(-1) === delim : m[2] === delim);
|
|
575
|
+
const items = [];
|
|
576
|
+
let loose = false;
|
|
577
|
+
let j = i;
|
|
578
|
+
while (j < lines.length) {
|
|
579
|
+
const m = RE.item.exec(lines[j]);
|
|
580
|
+
if (!sameList(m) || (items.length && m[1].length >= items.at(-1).indent)) break;
|
|
581
|
+
const gap = m[3].length >= 5 || !m[4] ? 1 : m[3].length;
|
|
582
|
+
const indent = m[1].length + m[2].length + gap;
|
|
583
|
+
const item = { indent, lines: [m[3].length >= 5 ? ' '.repeat(m[3].length - 1) + m[4] : m[4]] };
|
|
584
|
+
items.push(item);
|
|
585
|
+
j++;
|
|
586
|
+
let pendingBlank = 0;
|
|
587
|
+
while (j < lines.length) {
|
|
588
|
+
const line = lines[j];
|
|
589
|
+
if (RE.blank.test(line)) { pendingBlank++; j++; continue; }
|
|
590
|
+
const lead = line.match(/^ */)[0].length;
|
|
591
|
+
if (lead >= indent) {
|
|
592
|
+
for (; pendingBlank; pendingBlank--) item.lines.push('');
|
|
593
|
+
item.lines.push(line.slice(indent));
|
|
594
|
+
j++;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const next = RE.item.exec(line);
|
|
598
|
+
if (pendingBlank === 0 && !next && !startsBlock(lines, j, ctx)
|
|
599
|
+
&& !RE.blank.test(item.lines.at(-1) ?? '')) {
|
|
600
|
+
item.lines.push(line.trim()); // lazy paragraph continuation
|
|
601
|
+
j++;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
if (pendingBlank) {
|
|
607
|
+
const next = RE.item.exec(lines[j] ?? '');
|
|
608
|
+
if (j < lines.length && sameList(next) && next[1].length < indent) loose = true;
|
|
609
|
+
else { j -= pendingBlank; break; }
|
|
610
|
+
}
|
|
611
|
+
if (item.lines.slice(0, -1).some((l) => RE.blank.test(l)) && item.lines.length > 1) {
|
|
612
|
+
// A blank line between two of the item's own blocks.
|
|
613
|
+
const inner = item.lines.join('\n');
|
|
614
|
+
if (!/^(`{3,}|~{3,})/m.test(inner)) loose = true;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const tag = ordered ? 'ol' : 'ul';
|
|
618
|
+
const start = ordered ? parseInt(first[2], 10) : 1;
|
|
619
|
+
let html = start !== 1 ? `<${tag} start="${start}">\n` : `<${tag}>\n`;
|
|
620
|
+
for (const item of items) {
|
|
621
|
+
const body = renderBlocks(item.lines, ctx, { tight: !loose });
|
|
622
|
+
html += `<li>${body.replace(/\n$/, '')}</li>\n`;
|
|
623
|
+
}
|
|
624
|
+
return { html: html + `</${tag}>\n`, next: j };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function renderBlocks(lines, ctx, { tight = false } = {}) {
|
|
628
|
+
let out = '';
|
|
629
|
+
let i = 0;
|
|
630
|
+
while (i < lines.length) {
|
|
631
|
+
const line = lines[i];
|
|
632
|
+
if (RE.blank.test(line)) { i++; continue; }
|
|
633
|
+
|
|
634
|
+
const ruled = blockRuleAt(line, ctx);
|
|
635
|
+
if (ruled) {
|
|
636
|
+
let j = i + 1;
|
|
637
|
+
let closed = true;
|
|
638
|
+
if (ruled.rule.end) {
|
|
639
|
+
closed = false;
|
|
640
|
+
for (; j < lines.length; j++) {
|
|
641
|
+
if (ruled.rule.end.test(lines[j])) { closed = true; j++; break; }
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const html = ruled.rule.render({ lines: lines.slice(i, j), match: ruled.match, closed }, ctx.api);
|
|
645
|
+
if (html != null) {
|
|
646
|
+
out += String(html) + '\n';
|
|
647
|
+
i = j;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
let block = null;
|
|
653
|
+
if (RE.fence.test(line)) block = renderCode(lines, i, ctx);
|
|
654
|
+
else if (isMathBlockStart(line, ctx)) block = renderMathBlock(lines, i, ctx);
|
|
655
|
+
else if (RE.heading.test(line)) {
|
|
656
|
+
const [, hashes, text = ''] = RE.heading.exec(line);
|
|
657
|
+
const n = hashes.length;
|
|
658
|
+
block = { html: `<h${n}>${renderInline(text.trim(), ctx)}</h${n}>\n`, next: i + 1 };
|
|
659
|
+
} else if (RE.hr.test(line)) block = { html: '<hr>\n', next: i + 1 };
|
|
660
|
+
else if (RE.quote.test(line)) {
|
|
661
|
+
const inner = [];
|
|
662
|
+
let j = i;
|
|
663
|
+
for (; j < lines.length && RE.quote.test(lines[j]); j++) inner.push(lines[j].replace(RE.quote, ''));
|
|
664
|
+
block = { html: `<blockquote>\n${renderBlocks(inner, ctx)}</blockquote>\n`, next: j };
|
|
665
|
+
} else if (RE.item.test(line)) block = renderList(lines, i, ctx);
|
|
666
|
+
else if (isTableStart(lines, i)) block = renderTable(lines, i, ctx);
|
|
667
|
+
|
|
668
|
+
if (block) {
|
|
669
|
+
out += block.html;
|
|
670
|
+
i = block.next;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const para = [line];
|
|
675
|
+
let j = i + 1;
|
|
676
|
+
for (; j < lines.length && !RE.blank.test(lines[j]) && !startsBlock(lines, j, ctx); j++) para.push(lines[j]);
|
|
677
|
+
const inline = renderInline(para.map((l, n) => (n === 0 ? l.replace(/^[ \t]+/, '') : l.trimStart()))
|
|
678
|
+
.join('\n').replace(/[ \t]+$/, ''), ctx);
|
|
679
|
+
out += tight ? `${inline}\n` : `<p>${inline}</p>\n`;
|
|
680
|
+
i = j;
|
|
681
|
+
}
|
|
682
|
+
return out;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Render markdown to an HTML string. See the module comment for the subset,
|
|
687
|
+
* the safety rules and the options.
|
|
688
|
+
*
|
|
689
|
+
* @param {string} source
|
|
690
|
+
* @param {Object} [options]
|
|
691
|
+
* @param {boolean} [options.inline=false] - Inline markup only: no blocks, no `<p>`.
|
|
692
|
+
* @param {string[]} [options.allowHtml=[]] - Raw HTML tags that pass through, without attributes.
|
|
693
|
+
* @param {(code: string, lang: string) => (string|null)} [options.highlight] - Fenced code's HTML.
|
|
694
|
+
* @param {false|null|((tex: string, o: {display: boolean}) => (string|null))} [options.math]
|
|
695
|
+
* Omitted: `window.katex` when present. `false`: `$` is plain text.
|
|
696
|
+
* @param {Array<{pattern: RegExp, render: (match: RegExpExecArray, ctx: Object) => (string|null)}>} [options.inlineRules]
|
|
697
|
+
* @param {Array<{start: RegExp, end?: RegExp, render: (block: {lines: string[], match: RegExpExecArray, closed: boolean}, ctx: Object) => (string|null)}>} [options.blockRules]
|
|
698
|
+
* @returns {string}
|
|
699
|
+
*/
|
|
700
|
+
export function renderMarkdown(source, options = {}) {
|
|
701
|
+
const ctx = { opts: compileOptions(options ?? {}) };
|
|
702
|
+
ctx.api = Object.freeze({
|
|
703
|
+
escape: escapeHtml,
|
|
704
|
+
inline: (text) => renderInline(String(text ?? ''), ctx),
|
|
705
|
+
});
|
|
706
|
+
const text = String(source ?? '').replace(/\r\n?/g, '\n').replace(/\u0000/g, '�');
|
|
707
|
+
if (options?.inline) return renderInline(text, ctx);
|
|
708
|
+
return renderBlocks(text.split('\n'), ctx);
|
|
709
|
+
}
|