nexus-shared 1.1.20 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +903 -0
- package/dist/Client.Index.d.ts +44 -0
- package/dist/Client.Index.js +49 -0
- package/dist/Components/Documents/Button.d.ts +29 -0
- package/dist/Components/Documents/Button.js +11 -0
- package/dist/Components/Documents/Menu.d.ts +102 -0
- package/dist/Components/Documents/Menu.js +238 -0
- package/dist/Components/Documents/SplitButton.d.ts +64 -0
- package/dist/Components/Documents/SplitButton.js +127 -0
- package/dist/Components/Documents/TabButtons.d.ts +62 -0
- package/dist/Components/Documents/TabButtons.js +19 -0
- package/dist/Components/Forms/ApiForm.d.ts +21 -0
- package/dist/Components/Forms/ApiForm.js +99 -0
- package/dist/Components/Forms/Crud.d.ts +17 -0
- package/dist/Components/Forms/Crud.js +556 -0
- package/dist/Components/Forms/CrudParts.d.ts +15 -0
- package/dist/Components/Forms/CrudParts.js +36 -0
- package/dist/Components/Forms/Form.d.ts +52 -0
- package/dist/Components/Forms/Form.js +319 -0
- package/dist/Components/Forms/SubmitForm.d.ts +65 -0
- package/dist/Components/Forms/SubmitForm.js +198 -0
- package/dist/Components/Inputs/Calendar.d.ts +69 -0
- package/dist/Components/Inputs/Calendar.js +258 -0
- package/dist/Components/Inputs/CheckBox.d.ts +53 -0
- package/dist/Components/Inputs/CheckBox.js +87 -0
- package/dist/Components/Inputs/CheckBoxGroup.d.ts +20 -0
- package/dist/Components/Inputs/CheckBoxGroup.js +80 -0
- package/dist/Components/Inputs/ChoiceField.d.ts +122 -0
- package/dist/Components/Inputs/ChoiceField.js +169 -0
- package/dist/Components/Inputs/DatePicker.d.ts +54 -0
- package/dist/Components/Inputs/DatePicker.js +246 -0
- package/dist/Components/Inputs/DateRangePicker.d.ts +62 -0
- package/dist/Components/Inputs/DateRangePicker.js +283 -0
- package/dist/Components/Inputs/DateTimePicker.d.ts +75 -0
- package/dist/Components/Inputs/DateTimePicker.js +399 -0
- package/dist/Components/Inputs/Dropdown.d.ts +117 -0
- package/dist/Components/Inputs/Dropdown.js +839 -0
- package/dist/Components/Inputs/DropdownParts.d.ts +84 -0
- package/dist/Components/Inputs/DropdownParts.js +102 -0
- package/dist/Components/Inputs/EditorShell.d.ts +45 -0
- package/dist/Components/Inputs/EditorShell.js +214 -0
- package/dist/Components/Inputs/Field.d.ts +53 -0
- package/dist/Components/Inputs/Field.js +16 -0
- package/dist/Components/Inputs/GroupForm.d.ts +20 -0
- package/dist/Components/Inputs/GroupForm.js +170 -0
- package/dist/Components/Inputs/InputField.d.ts +59 -0
- package/dist/Components/Inputs/InputField.js +169 -0
- package/dist/Components/Inputs/InputRenderer.d.ts +35 -0
- package/dist/Components/Inputs/InputRenderer.js +99 -0
- package/dist/Components/Inputs/MarkdownEditor.d.ts +67 -0
- package/dist/Components/Inputs/MarkdownEditor.js +289 -0
- package/dist/Components/Inputs/NumberBox.d.ts +53 -0
- package/dist/Components/Inputs/NumberBox.js +228 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +20 -0
- package/dist/Components/Inputs/RadioGroup.js +43 -0
- package/dist/Components/Inputs/ReadOnlyNotice.d.ts +36 -0
- package/dist/Components/Inputs/ReadOnlyNotice.js +116 -0
- package/dist/Components/Inputs/RichTextEditor.d.ts +103 -0
- package/dist/Components/Inputs/RichTextEditor.js +1170 -0
- package/dist/Components/Inputs/RichTextParts.d.ts +63 -0
- package/dist/Components/Inputs/RichTextParts.js +157 -0
- package/dist/Components/Inputs/RowsInput.d.ts +104 -0
- package/dist/Components/Inputs/RowsInput.js +259 -0
- package/dist/Components/Inputs/Slider.d.ts +87 -0
- package/dist/Components/Inputs/Slider.js +216 -0
- package/dist/Components/Inputs/TabularForm.d.ts +23 -0
- package/dist/Components/Inputs/TabularForm.js +283 -0
- package/dist/Components/Inputs/TextArea.d.ts +43 -0
- package/dist/Components/Inputs/TextArea.js +31 -0
- package/dist/Components/Inputs/TextBox.d.ts +52 -0
- package/dist/Components/Inputs/TextBox.js +58 -0
- package/dist/Components/Inputs/TextField.d.ts +31 -0
- package/dist/Components/Inputs/TextField.js +35 -0
- package/dist/Components/Inputs/TimePanel.d.ts +34 -0
- package/dist/Components/Inputs/TimePanel.js +288 -0
- package/dist/Components/Inputs/TimePicker.d.ts +54 -0
- package/dist/Components/Inputs/TimePicker.js +239 -0
- package/dist/Components/Inputs/TreeView.d.ts +160 -0
- package/dist/Components/Inputs/TreeView.js +947 -0
- package/dist/Components/Layouts/MessageBox.d.ts +71 -0
- package/dist/Components/Layouts/MessageBox.js +104 -0
- package/dist/Components/Layouts/Popup.d.ts +92 -0
- package/dist/Components/Layouts/Popup.js +376 -0
- package/dist/Components/Layouts/PopupContext.d.ts +2 -0
- package/dist/Components/Layouts/PopupContext.js +4 -0
- package/dist/Components/Layouts/PopupDock.d.ts +22 -0
- package/dist/Components/Layouts/PopupDock.js +121 -0
- package/dist/Components/Layouts/PopupHost.d.ts +56 -0
- package/dist/Components/Layouts/PopupHost.js +163 -0
- package/dist/Components/Layouts/PopupRules.d.ts +30 -0
- package/dist/Components/Layouts/PopupRules.js +63 -0
- package/dist/Components/Layouts/ThemePicker.d.ts +13 -0
- package/dist/Components/Layouts/ThemePicker.js +22 -0
- package/dist/Components/Layouts/ThemeScript.d.ts +14 -0
- package/dist/Components/Layouts/ThemeScript.js +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.js +29 -0
- package/dist/Components/Layouts/Toaster.d.ts +76 -0
- package/dist/Components/Layouts/Toaster.js +172 -0
- package/dist/Components/Viewers/DataTable.d.ts +25 -0
- package/dist/Components/Viewers/DataTable.js +542 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +134 -0
- package/dist/Components/Viewers/DataTableParts.js +145 -0
- package/dist/Components/Viewers/MarkdownView.d.ts +26 -0
- package/dist/Components/Viewers/MarkdownView.js +79 -0
- package/dist/Components/Viewers/RichTextView.d.ts +18 -0
- package/dist/Components/Viewers/RichTextView.js +17 -0
- package/dist/Helpers/AnimationHelpers.d.ts +6 -0
- package/dist/Helpers/AnimationHelpers.js +35 -0
- package/dist/Helpers/ApiClient.d.ts +74 -0
- package/dist/Helpers/ApiClient.js +197 -0
- package/dist/Helpers/ApiFormHelpers.d.ts +26 -0
- package/dist/Helpers/ApiFormHelpers.js +56 -0
- package/dist/Helpers/ApiModules.d.ts +135 -0
- package/dist/Helpers/ApiModules.js +194 -0
- package/dist/Helpers/ApiResponses.d.ts +41 -0
- package/dist/Helpers/ApiResponses.js +159 -0
- package/dist/Helpers/ApiRoutes.d.ts +150 -0
- package/dist/Helpers/ApiRoutes.js +191 -0
- package/dist/Helpers/ApiToasts.d.ts +21 -0
- package/dist/Helpers/ApiToasts.js +22 -0
- package/dist/Helpers/ClassHelpers.d.ts +3 -0
- package/dist/Helpers/ClassHelpers.js +4 -0
- package/dist/Helpers/CrudBackend.d.ts +56 -0
- package/dist/Helpers/CrudBackend.js +119 -0
- package/dist/Helpers/CrudHelpers.d.ts +76 -0
- package/dist/Helpers/CrudHelpers.js +202 -0
- package/dist/Helpers/DateHelpers.d.ts +88 -0
- package/dist/Helpers/DateHelpers.js +363 -0
- package/dist/Helpers/DatePreferences.d.ts +14 -0
- package/dist/Helpers/DatePreferences.js +94 -0
- package/dist/Helpers/DateTimeHelpers.d.ts +87 -0
- package/dist/Helpers/DateTimeHelpers.js +286 -0
- package/dist/Helpers/DragHelpers.d.ts +129 -0
- package/dist/Helpers/DragHelpers.js +281 -0
- package/dist/Helpers/DropdownHelpers.d.ts +56 -0
- package/dist/Helpers/DropdownHelpers.js +180 -0
- package/dist/Helpers/FormDrafts.d.ts +18 -0
- package/dist/Helpers/FormDrafts.js +76 -0
- package/dist/Helpers/FormStore.d.ts +231 -0
- package/dist/Helpers/FormStore.js +734 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +79 -0
- package/dist/Helpers/InputParamsHelpers.js +388 -0
- package/dist/Helpers/MarkdownEditing.d.ts +27 -0
- package/dist/Helpers/MarkdownEditing.js +250 -0
- package/dist/Helpers/MarkdownHelpers.d.ts +82 -0
- package/dist/Helpers/MarkdownHelpers.js +775 -0
- package/dist/Helpers/MessageBuilder.d.ts +74 -0
- package/dist/Helpers/MessageBuilder.js +254 -0
- package/dist/Helpers/NepaliCalendar.d.ts +25 -0
- package/dist/Helpers/NepaliCalendar.js +107 -0
- package/dist/Helpers/NumberHelpers.d.ts +71 -0
- package/dist/Helpers/NumberHelpers.js +202 -0
- package/dist/Helpers/Permissions.d.ts +62 -0
- package/dist/Helpers/Permissions.js +133 -0
- package/dist/Helpers/PopoverHelpers.d.ts +43 -0
- package/dist/Helpers/PopoverHelpers.js +91 -0
- package/dist/Helpers/RichTextHelpers.d.ts +77 -0
- package/dist/Helpers/RichTextHelpers.js +1072 -0
- package/dist/Helpers/RowsStore.d.ts +160 -0
- package/dist/Helpers/RowsStore.js +584 -0
- package/dist/Helpers/TableColumns.d.ts +40 -0
- package/dist/Helpers/TableColumns.js +254 -0
- package/dist/Helpers/TableExport.d.ts +30 -0
- package/dist/Helpers/TableExport.js +97 -0
- package/dist/Helpers/TableHelpers.d.ts +102 -0
- package/dist/Helpers/TableHelpers.js +281 -0
- package/dist/Helpers/TableStore.d.ts +142 -0
- package/dist/Helpers/TableStore.js +530 -0
- package/dist/Helpers/ThemeHelpers.d.ts +15 -0
- package/dist/Helpers/ThemeHelpers.js +16 -0
- package/dist/Helpers/TimeHelpers.d.ts +73 -0
- package/dist/Helpers/TimeHelpers.js +253 -0
- package/dist/Helpers/TreeStore.d.ts +188 -0
- package/dist/Helpers/TreeStore.js +577 -0
- package/dist/Helpers/ValidationHelpers.d.ts +124 -0
- package/dist/Helpers/ValidationHelpers.js +301 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +149 -0
- package/dist/Interfaces/ApiInterfaces.js +24 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +171 -0
- package/{src/sso-client.tsx → dist/Interfaces/CrudInterfaces.js} +1 -1
- package/dist/Interfaces/DateInterfaces.d.ts +154 -0
- package/dist/Interfaces/DateInterfaces.js +29 -0
- package/dist/Interfaces/FormInterfaces.d.ts +352 -0
- package/dist/Interfaces/FormInterfaces.js +1 -0
- package/dist/Interfaces/InputInterfaces.d.ts +141 -0
- package/dist/Interfaces/InputInterfaces.js +1 -0
- package/dist/Interfaces/MessageInterfaces.d.ts +111 -0
- package/dist/Interfaces/MessageInterfaces.js +5 -0
- package/dist/Interfaces/TableInterfaces.d.ts +291 -0
- package/dist/Interfaces/TableInterfaces.js +1 -0
- package/dist/Interfaces/ThemeInterfaces.d.ts +17 -0
- package/dist/Interfaces/ThemeInterfaces.js +17 -0
- package/dist/Interfaces/TypeInterfaces.d.ts +4 -0
- package/dist/Interfaces/TypeInterfaces.js +1 -0
- package/dist/Server.Index.d.ts +2 -0
- package/dist/Server.Index.js +5 -0
- package/dist/Services/ApiProxy.d.ts +32 -0
- package/dist/Services/ApiProxy.js +111 -0
- package/dist/Services/BrowserApi.d.ts +78 -0
- package/dist/Services/BrowserApi.js +111 -0
- package/dist/Services/ServerApi.d.ts +49 -0
- package/dist/Services/ServerApi.js +75 -0
- package/dist/Services/ThemeService.d.ts +17 -0
- package/dist/Services/ThemeService.js +83 -0
- package/dist/Shared.Index.d.ts +43 -0
- package/dist/Shared.Index.js +47 -0
- package/package.json +82 -33
- package/src/Styles/Nexus.Base.css +70 -0
- package/src/Styles/Nexus.Button.css +151 -0
- package/src/Styles/Nexus.Calendar.css +275 -0
- package/src/Styles/Nexus.Choice.css +345 -0
- package/src/Styles/Nexus.Crud.css +84 -0
- package/src/Styles/Nexus.DateTime.css +81 -0
- package/src/Styles/Nexus.Dropdown.css +774 -0
- package/src/Styles/Nexus.Editor.css +297 -0
- package/src/Styles/Nexus.Form.css +298 -0
- package/src/Styles/Nexus.Index.css +34 -0
- package/src/Styles/Nexus.Input.css +637 -0
- package/src/Styles/Nexus.Markdown.css +265 -0
- package/src/Styles/Nexus.Menu.css +249 -0
- package/src/Styles/Nexus.Popup.css +731 -0
- package/src/Styles/Nexus.RichText.css +463 -0
- package/src/Styles/Nexus.Rows.css +566 -0
- package/src/Styles/Nexus.Slider.css +278 -0
- package/src/Styles/Nexus.Tab.Buttons.css +364 -0
- package/src/Styles/Nexus.Table.css +1014 -0
- package/src/Styles/Nexus.Theme.Picker.css +159 -0
- package/src/Styles/Nexus.Themes.css +194 -0
- package/src/Styles/Nexus.Time.css +147 -0
- package/src/Styles/Nexus.Toast.css +265 -0
- package/src/Styles/Nexus.Tokens.css +72 -0
- package/src/Styles/Nexus.Tree.css +435 -0
- package/src/api-services/authentication-service.tsx +0 -23
- package/src/api-services/preference-service.tsx +0 -5
- package/src/api-services/system-service.tsx +0 -34
- package/src/client.ts +0 -31
- package/src/components/documents/button.tsx +0 -142
- package/src/components/documents/icon-box.tsx +0 -93
- package/src/components/documents/page-title.tsx +0 -7
- package/src/components/documents/tab-button.tsx +0 -172
- package/src/components/documents/tag.tsx +0 -30
- package/src/components/index.js +0 -0
- package/src/components/inputs/checkbox-input.tsx +0 -66
- package/src/components/inputs/input-box.tsx +0 -48
- package/src/components/inputs/input-element.tsx +0 -65
- package/src/components/inputs/input-form.tsx +0 -164
- package/src/components/inputs/input.tsx +0 -181
- package/src/components/inputs/number-input.tsx +0 -108
- package/src/components/inputs/radiobox-input.tsx +0 -53
- package/src/components/inputs/textarea-input.tsx +0 -47
- package/src/components/inputs/textbox-input.tsx +0 -45
- package/src/components/layouts/global-dialogbox.tsx +0 -431
- package/src/components/layouts/global-layout.tsx +0 -59
- package/src/components/layouts/layout-helpers.tsx +0 -20
- package/src/components/layouts/panels/user-panel.tsx +0 -112
- package/src/components/layouts/utility-menu.tsx +0 -49
- package/src/components/panels/theme-panel.tsx +0 -46
- package/src/helpers/bitwise-helpers.tsx +0 -11
- package/src/helpers/browser-helpers.tsx +0 -156
- package/src/helpers/datasource-helpers.tsx +0 -99
- package/src/helpers/element-helpers.tsx +0 -57
- package/src/helpers/input-helpers.tsx +0 -74
- package/src/helpers/string-helpers.tsx +0 -28
- package/src/helpers/utility-helpers.tsx +0 -45
- package/src/helpers/validation-helpers.tsx +0 -302
- package/src/index.ts +0 -26
- package/src/interface.ts +0 -19
- package/src/interfaces/auth-token-interfaces.tsx +0 -6
- package/src/interfaces/browser-interfaces.tsx +0 -36
- package/src/interfaces/button-interfaces.tsx +0 -63
- package/src/interfaces/datasource-interfaces.tsx +0 -22
- package/src/interfaces/datatable-interfaces.tsx +0 -25
- package/src/interfaces/dialogbox-interfaces.tsx +0 -5
- package/src/interfaces/exception-interfaces.tsx +0 -99
- package/src/interfaces/http-interfaces.tsx +0 -129
- package/src/interfaces/icon-interfaces.tsx +0 -129
- package/src/interfaces/input-interfaces.tsx +0 -410
- package/src/interfaces/layout-interfaces.tsx +0 -191
- package/src/interfaces/menu-interfaces.tsx +0 -49
- package/src/interfaces/message-interfaces.tsx +0 -32
- package/src/interfaces/permission-interfaces.tsx +0 -9
- package/src/interfaces/storage-interfaces.tsx +0 -5
- package/src/interfaces/system-interfaces.tsx +0 -22
- package/src/interfaces/theme-interfaces.tsx +0 -123
- package/src/interfaces/type-interfaces.tsx +0 -28
- package/src/interfaces/user-interfaces.tsx +0 -47
- package/src/nexus-client.tsx +0 -21
- package/src/nexus.environments.tsx +0 -66
- package/src/proxy-api/proxy-backend.tsx +0 -60
- package/src/proxy-api/proxy-constants.tsx +0 -28
- package/src/proxy-api/proxy-helpers.tsx +0 -91
- package/src/proxy-api-client.tsx +0 -1
- package/src/proxy-api-server.tsx +0 -2
- package/src/services/http-client-services.tsx +0 -75
- package/src/services/http-services.tsx +0 -158
- package/src/services/loader-service.tsx +0 -185
- package/src/services/localstorage-service.tsx +0 -119
- package/src/services/message-services.tsx +0 -383
- package/src/services/theme-service.tsx +0 -161
- package/src/services/user-services.tsx +0 -10
- package/src/sso-config/auth-token-validation.ts +0 -16
- package/src/sso-config/callback-route.tsx +0 -35
- package/src/sso-config/cookie-encryption.ts +0 -51
- package/src/sso-config/cookie-helpers.tsx +0 -102
- package/src/sso-config/forgot-password-route.tsx +0 -36
- package/src/sso-config/oauth-callback-state.ts +0 -61
- package/src/sso-config/pkce-helpers.ts +0 -9
- package/src/sso-config/provider-complete-route.tsx +0 -18
- package/src/sso-config/redirect-context-actions.tsx +0 -45
- package/src/sso-config/redirect-context-helpers.tsx +0 -159
- package/src/sso-config/redirect-context-persist.tsx +0 -18
- package/src/sso-config/redirect-context-route.tsx +0 -45
- package/src/sso-config/refresh-route.tsx +0 -23
- package/src/sso-config/sign-in-route.tsx +0 -74
- package/src/sso-config/sign-out-route.tsx +0 -57
- package/src/sso-config/sign-up-route.tsx +0 -57
- package/src/sso-config/sso-backend-flow.ts +0 -38
- package/src/sso-config/sso-complete-route.tsx +0 -50
- package/src/sso-config/sso-config.ts +0 -59
- package/src/sso-config/sso-interfaces.tsx +0 -99
- package/src/sso-config/sso-response-helpers.tsx +0 -20
- package/src/sso-server.tsx +0 -21
- package/src/styles/nexus.animation.css +0 -269
- package/src/styles/nexus.core.css +0 -119
- package/src/styles/nexus.dialog.css +0 -144
- package/src/styles/nexus.icon.css +0 -51
- package/src/styles/nexus.input.css +0 -207
- package/src/styles/nexus.loader.css +0 -11
- package/src/styles/nexus.logic.css +0 -47
- package/src/styles/nexus.utility.css +0 -347
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Markdown: a small parser for GitHub-flavored Markdown, shared by MarkdownEditor and MarkdownView. It has no
|
|
3
|
+
* dependencies and no DOM, so it runs on the server, in the browser, and in tests. Raw HTML is never rendered (it stays
|
|
4
|
+
* text), and links and images keep only safe addresses, so text typed by any user can be shown as it is.
|
|
5
|
+
*
|
|
6
|
+
* Supported: headings (# and underlined), paragraphs and line breaks, bold, italic, strikethrough, inline code, code
|
|
7
|
+
* blocks (fenced and indented), links (inline, reference, <autolinks>, and bare URLs), images, block quotes, bulleted,
|
|
8
|
+
* numbered, and task lists, tables with column alignment, horizontal lines, backslash escapes, and HTML entities.
|
|
9
|
+
*/
|
|
10
|
+
/** Parses Markdown into blocks for rendering. Never throws, whatever the text. */
|
|
11
|
+
export function parseMarkdown(markdown, options = {}) {
|
|
12
|
+
const lines = markdown.split(/\r\n?|\n/).map((text, n) => ({ text: expandTabs(text), n }));
|
|
13
|
+
const context = { refs: new Map(), pending: [] };
|
|
14
|
+
const blocks = parseBlocks(lines, context);
|
|
15
|
+
const inline = { refs: context.refs, breaks: options.breaks ?? true };
|
|
16
|
+
for (const { node, raw } of context.pending)
|
|
17
|
+
node.children = parseInline(raw, inline);
|
|
18
|
+
return blocks;
|
|
19
|
+
}
|
|
20
|
+
/** The text of Markdown without its marks, for excerpts, search, and counts. Blocks are separated by a line break. */
|
|
21
|
+
export function markdownToPlainText(markdown) {
|
|
22
|
+
return blocksText(parseMarkdown(markdown)).trim();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The address when a link or image may use it, otherwise `null`. Links allow http, https, mailto, tel, and addresses
|
|
26
|
+
* without a scheme (relative paths and #anchors); images allow http, https, relative paths, and raster data URLs.
|
|
27
|
+
*/
|
|
28
|
+
export function safeUrl(url, kind = "link") {
|
|
29
|
+
const trimmed = url.trim();
|
|
30
|
+
if (!trimmed)
|
|
31
|
+
return null;
|
|
32
|
+
// Browsers ignore spaces and control characters inside a scheme ("java\tscript:"), so they are ignored here too.
|
|
33
|
+
const compact = trimmed.replace(/[\s\p{Cc}]/gu, "").toLowerCase();
|
|
34
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(compact)?.[1];
|
|
35
|
+
if (!scheme || scheme === "http" || scheme === "https")
|
|
36
|
+
return trimmed;
|
|
37
|
+
if (kind === "link" && (scheme === "mailto" || scheme === "tel"))
|
|
38
|
+
return trimmed;
|
|
39
|
+
if (kind === "image" && /^data:image\/(?:png|gif|jpe?g|webp|avif);/.test(compact))
|
|
40
|
+
return trimmed;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const FENCE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
44
|
+
const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
|
|
45
|
+
const HEADING = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$/;
|
|
46
|
+
const RULE = /^ {0,3}(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/;
|
|
47
|
+
const QUOTE = /^ {0,3}> ?(.*)$/;
|
|
48
|
+
const LIST_ITEM = /^( {0,3})([-+*]|\d{1,9}[.)])(?:([ \t]+)(.*))?$/;
|
|
49
|
+
const TASK = /^\[([ xX])\](?:[ \t]+|$)/;
|
|
50
|
+
const SETEXT = /^ {0,3}(=+|-+)[ \t]*$/;
|
|
51
|
+
const TABLE_DELIMITER = /^ {0,3}\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/;
|
|
52
|
+
const REFERENCE = /^ {0,3}\[((?:[^[\]\\]|\\.)+)\]:[ \t]*(<[^<>]*>|\S+)(?:[ \t]+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\((?:[^()\\]|\\.)*\)))?[ \t]*$/;
|
|
53
|
+
function parseBlocks(lines, context) {
|
|
54
|
+
const blocks = [];
|
|
55
|
+
let i = 0;
|
|
56
|
+
while (i < lines.length) {
|
|
57
|
+
const text = lines[i].text;
|
|
58
|
+
if (isBlank(text)) {
|
|
59
|
+
i++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Indented code: four spaces. A paragraph's indented continuation lines never get here.
|
|
63
|
+
if (indentOf(text) >= 4) {
|
|
64
|
+
const code = [];
|
|
65
|
+
while (i < lines.length && (isBlank(lines[i].text) || indentOf(lines[i].text) >= 4))
|
|
66
|
+
code.push(lines[i++].text.slice(4));
|
|
67
|
+
while (code.length && isBlank(code[code.length - 1]))
|
|
68
|
+
code.pop();
|
|
69
|
+
blocks.push({ type: "code", language: "", text: code.join("\n") });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const fence = openFence(text);
|
|
73
|
+
if (fence) {
|
|
74
|
+
const code = [];
|
|
75
|
+
let end = i + 1;
|
|
76
|
+
for (; end < lines.length; end++) {
|
|
77
|
+
const close = FENCE_CLOSE.exec(lines[end].text)?.[1];
|
|
78
|
+
if (close && close[0] === fence.marks[0] && close.length >= fence.marks.length)
|
|
79
|
+
break;
|
|
80
|
+
code.push(removeIndent(lines[end].text, fence.indent));
|
|
81
|
+
}
|
|
82
|
+
blocks.push({ type: "code", language: fence.language, text: code.join("\n") });
|
|
83
|
+
i = end + 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const heading = HEADING.exec(text);
|
|
87
|
+
if (heading) {
|
|
88
|
+
const node = { type: "heading", level: heading[1].length, children: [] };
|
|
89
|
+
// A closing run of # is not part of the heading: "## Title ##".
|
|
90
|
+
context.pending.push({ node, raw: (heading[2] ?? "").replace(/(?:^|[ \t]+)#+$/, "") });
|
|
91
|
+
blocks.push(node);
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (RULE.test(text)) {
|
|
96
|
+
blocks.push({ type: "rule" });
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
else if (QUOTE.test(text)) {
|
|
100
|
+
i = parseQuote(lines, i, context, blocks);
|
|
101
|
+
}
|
|
102
|
+
else if (LIST_ITEM.test(text)) {
|
|
103
|
+
i = parseList(lines, i, context, blocks);
|
|
104
|
+
}
|
|
105
|
+
else if (isTableStart(lines, i)) {
|
|
106
|
+
i = parseTable(lines, i, context, blocks);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
i = parseParagraph(lines, i, context, blocks);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return blocks;
|
|
113
|
+
}
|
|
114
|
+
function parseQuote(lines, start, context, blocks) {
|
|
115
|
+
const inner = [];
|
|
116
|
+
let i = start;
|
|
117
|
+
for (; i < lines.length; i++) {
|
|
118
|
+
const line = lines[i];
|
|
119
|
+
const quoted = QUOTE.exec(line.text);
|
|
120
|
+
if (quoted) {
|
|
121
|
+
inner.push({ text: quoted[1], n: line.n });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// A line without ">" right after quoted text still belongs to the quoted paragraph.
|
|
125
|
+
const last = inner[inner.length - 1].text;
|
|
126
|
+
if (isBlank(line.text) || !continuesParagraph(last) || interruptsParagraph(line.text))
|
|
127
|
+
break;
|
|
128
|
+
inner.push(line);
|
|
129
|
+
}
|
|
130
|
+
blocks.push({ type: "quote", children: parseBlocks(inner, context) });
|
|
131
|
+
return i;
|
|
132
|
+
}
|
|
133
|
+
function parseList(lines, start, context, blocks) {
|
|
134
|
+
const first = LIST_ITEM.exec(lines[start].text);
|
|
135
|
+
const kind = markerKind(first[2]);
|
|
136
|
+
const ordered = kind === "." || kind === ")";
|
|
137
|
+
const list = { type: "list", ordered, start: ordered ? parseInt(first[2], 10) : 1, tight: true, items: [] };
|
|
138
|
+
let i = start;
|
|
139
|
+
while (i < lines.length) {
|
|
140
|
+
const item = LIST_ITEM.exec(lines[i].text);
|
|
141
|
+
if (!item || RULE.test(lines[i].text) || markerKind(item[2]) !== kind)
|
|
142
|
+
break;
|
|
143
|
+
const [, pad = "", marker = "", gap = "", rest = ""] = item;
|
|
144
|
+
// The item's text starts after the marker and its spaces. Five or more spaces start indented code, one space in.
|
|
145
|
+
const gapWidth = !rest || gap.length > 4 ? 1 : gap.length;
|
|
146
|
+
const indent = pad.length + marker.length + gapWidth;
|
|
147
|
+
const content = [{ text: gap.length > 4 && rest ? " ".repeat(gap.length - 1) + rest : rest, n: lines[i].n }];
|
|
148
|
+
let end = i + 1;
|
|
149
|
+
for (; end < lines.length; end++) {
|
|
150
|
+
const line = lines[end];
|
|
151
|
+
if (isBlank(line.text)) {
|
|
152
|
+
content.push({ text: "", n: line.n });
|
|
153
|
+
}
|
|
154
|
+
else if (indentOf(line.text) >= indent) {
|
|
155
|
+
content.push({ text: line.text.slice(indent), n: line.n });
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// A line that is not indented still continues the item's paragraph, unless it starts something else.
|
|
159
|
+
const last = content[content.length - 1].text;
|
|
160
|
+
if (LIST_ITEM.test(line.text) || !continuesParagraph(last) || interruptsParagraph(line.text))
|
|
161
|
+
break;
|
|
162
|
+
content.push({ text: line.text.trimStart(), n: line.n });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Blank lines after the item separate it from the next one; they are not part of it.
|
|
166
|
+
let size = content.length;
|
|
167
|
+
while (size > 1 && isBlank(content[size - 1].text))
|
|
168
|
+
size--;
|
|
169
|
+
const blankAfter = size < content.length;
|
|
170
|
+
content.length = size;
|
|
171
|
+
let checked = null;
|
|
172
|
+
const task = TASK.exec(content[0].text);
|
|
173
|
+
if (task) {
|
|
174
|
+
checked = task[1] !== " ";
|
|
175
|
+
content[0] = { text: content[0].text.slice(task[0].length), n: content[0].n };
|
|
176
|
+
}
|
|
177
|
+
const children = parseBlocks(content, context);
|
|
178
|
+
// A blank line between two blocks of one item makes the whole list loose.
|
|
179
|
+
if (children.length > 1 && content.some(line => isBlank(line.text)))
|
|
180
|
+
list.tight = false;
|
|
181
|
+
list.items.push({ checked, line: lines[i].n, children });
|
|
182
|
+
i = end;
|
|
183
|
+
// So does a blank line between two items.
|
|
184
|
+
const next = blankAfter && i < lines.length ? LIST_ITEM.exec(lines[i].text) : null;
|
|
185
|
+
if (next && markerKind(next[2]) === kind && !RULE.test(lines[i].text))
|
|
186
|
+
list.tight = false;
|
|
187
|
+
}
|
|
188
|
+
blocks.push(list);
|
|
189
|
+
return i;
|
|
190
|
+
}
|
|
191
|
+
function parseTable(lines, start, context, blocks) {
|
|
192
|
+
const head = splitRow(lines[start].text);
|
|
193
|
+
const align = splitRow(lines[start + 1].text).map((cell) => {
|
|
194
|
+
const left = cell.startsWith(":");
|
|
195
|
+
const right = cell.endsWith(":");
|
|
196
|
+
return left && right ? "center" : right ? "right" : left ? "left" : null;
|
|
197
|
+
});
|
|
198
|
+
const rows = [];
|
|
199
|
+
let i = start + 2;
|
|
200
|
+
for (; i < lines.length; i++) {
|
|
201
|
+
const text = lines[i].text;
|
|
202
|
+
if (isBlank(text) || interruptsParagraph(text))
|
|
203
|
+
break;
|
|
204
|
+
const cells = splitRow(text);
|
|
205
|
+
// Every row has the header's columns: missing cells are empty and extra cells are dropped.
|
|
206
|
+
rows.push(head.map((_, k) => tableCell(context, cells[k] ?? "")));
|
|
207
|
+
}
|
|
208
|
+
blocks.push({ type: "table", align, head: head.map(cell => tableCell(context, cell)), rows });
|
|
209
|
+
return i;
|
|
210
|
+
}
|
|
211
|
+
function parseParagraph(lines, start, context, blocks) {
|
|
212
|
+
const raw = [];
|
|
213
|
+
let level = 0;
|
|
214
|
+
let i = start;
|
|
215
|
+
for (; i < lines.length; i++) {
|
|
216
|
+
const text = lines[i].text;
|
|
217
|
+
if (isBlank(text))
|
|
218
|
+
break;
|
|
219
|
+
if (raw.length) {
|
|
220
|
+
// A line of = or - under text turns the paragraph into a heading.
|
|
221
|
+
const underline = SETEXT.exec(text);
|
|
222
|
+
if (underline) {
|
|
223
|
+
level = underline[1][0] === "=" ? 1 : 2;
|
|
224
|
+
i++;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
if (interruptsParagraph(text) || isTableStart(lines, i))
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
raw.push(text.trimStart());
|
|
231
|
+
}
|
|
232
|
+
let first = 0;
|
|
233
|
+
for (; first < raw.length; first++) {
|
|
234
|
+
const definition = REFERENCE.exec(raw[first]);
|
|
235
|
+
const key = definition ? normalizeLabel(definition[1]) : "";
|
|
236
|
+
if (!key)
|
|
237
|
+
break;
|
|
238
|
+
const destination = definition[2];
|
|
239
|
+
const title = definition[3];
|
|
240
|
+
if (!context.refs.has(key)) {
|
|
241
|
+
context.refs.set(key, {
|
|
242
|
+
href: unescapeText(destination.startsWith("<") ? destination.slice(1, -1) : destination),
|
|
243
|
+
title: title ? unescapeText(title.slice(1, -1)) : undefined,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (first < raw.length) {
|
|
248
|
+
const node = level ? { type: "heading", level, children: [] } : { type: "paragraph", children: [] };
|
|
249
|
+
// Spaces at the end of the last line are not a line break.
|
|
250
|
+
context.pending.push({ node, raw: raw.slice(first).join("\n").trimEnd() });
|
|
251
|
+
blocks.push(node);
|
|
252
|
+
}
|
|
253
|
+
return i;
|
|
254
|
+
}
|
|
255
|
+
function openFence(text) {
|
|
256
|
+
const match = FENCE.exec(text);
|
|
257
|
+
if (!match)
|
|
258
|
+
return null;
|
|
259
|
+
const [, indent = "", marks = "", info = ""] = match;
|
|
260
|
+
// A backtick fence's language cannot hold a backtick: "```a``` b" is inline code.
|
|
261
|
+
if (marks[0] === "`" && info.includes("`"))
|
|
262
|
+
return null;
|
|
263
|
+
return { indent: indent.length, marks, language: unescapeText(info.trim().split(/[ \t]/)[0] ?? "") };
|
|
264
|
+
}
|
|
265
|
+
/** Whether a line starts a block that ends the paragraph above it. */
|
|
266
|
+
function interruptsParagraph(text) {
|
|
267
|
+
if (openFence(text) || HEADING.test(text) || RULE.test(text) || QUOTE.test(text))
|
|
268
|
+
return true;
|
|
269
|
+
const item = LIST_ITEM.exec(text);
|
|
270
|
+
// Inside a paragraph, only a list item with text starts a list, and a numbered one only from 1.
|
|
271
|
+
return Boolean(item && item[4]?.trim() && (!/\d/.test(item[2]) || parseInt(item[2], 10) === 1));
|
|
272
|
+
}
|
|
273
|
+
/** Whether a line is paragraph text that a following line without its container's marker can continue. */
|
|
274
|
+
function continuesParagraph(text) {
|
|
275
|
+
return !isBlank(text) && indentOf(text) < 4 && !openFence(text) && !HEADING.test(text) && !RULE.test(text);
|
|
276
|
+
}
|
|
277
|
+
function isTableStart(lines, i) {
|
|
278
|
+
const header = lines[i].text;
|
|
279
|
+
const delimiter = lines[i + 1]?.text;
|
|
280
|
+
return Boolean(delimiter &&
|
|
281
|
+
header.includes("|") &&
|
|
282
|
+
delimiter.includes("|") &&
|
|
283
|
+
TABLE_DELIMITER.test(delimiter) &&
|
|
284
|
+
splitRow(header).length === splitRow(delimiter).length);
|
|
285
|
+
}
|
|
286
|
+
/** Cells of a table row. A pipe escaped with a backslash stays in its cell, also inside code. */
|
|
287
|
+
function splitRow(text) {
|
|
288
|
+
let row = text.trim();
|
|
289
|
+
if (row.startsWith("|"))
|
|
290
|
+
row = row.slice(1);
|
|
291
|
+
if (row.endsWith("|") && !row.endsWith("\\|"))
|
|
292
|
+
row = row.slice(0, -1);
|
|
293
|
+
const cells = [];
|
|
294
|
+
let cell = "";
|
|
295
|
+
for (let k = 0; k < row.length; k++) {
|
|
296
|
+
const char = row[k];
|
|
297
|
+
if (char === "\\" && row[k + 1] === "|") {
|
|
298
|
+
cell += "|";
|
|
299
|
+
k++;
|
|
300
|
+
}
|
|
301
|
+
else if (char === "|") {
|
|
302
|
+
cells.push(cell.trim());
|
|
303
|
+
cell = "";
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
cell += char;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
cells.push(cell.trim());
|
|
310
|
+
return cells;
|
|
311
|
+
}
|
|
312
|
+
function tableCell(context, raw) {
|
|
313
|
+
const cell = { children: [] };
|
|
314
|
+
context.pending.push({ node: cell, raw });
|
|
315
|
+
return cell;
|
|
316
|
+
}
|
|
317
|
+
const markerKind = (marker) => (/\d/.test(marker) ? marker.slice(-1) : marker);
|
|
318
|
+
const isBlank = (text) => !/\S/.test(text);
|
|
319
|
+
const indentOf = (text) => text.length - text.replace(/^ +/, "").length;
|
|
320
|
+
function removeIndent(text, width) {
|
|
321
|
+
let k = 0;
|
|
322
|
+
while (k < width && text[k] === " ")
|
|
323
|
+
k++;
|
|
324
|
+
return text.slice(k);
|
|
325
|
+
}
|
|
326
|
+
/** Tabs in a line's indent become spaces, to the next multiple of 4, so indents compare by width. */
|
|
327
|
+
function expandTabs(text) {
|
|
328
|
+
if (!text.includes("\t"))
|
|
329
|
+
return text;
|
|
330
|
+
let column = 0;
|
|
331
|
+
let k = 0;
|
|
332
|
+
for (; k < text.length; k++) {
|
|
333
|
+
if (text[k] === " ")
|
|
334
|
+
column++;
|
|
335
|
+
else if (text[k] === "\t")
|
|
336
|
+
column += 4 - (column % 4);
|
|
337
|
+
else
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
return " ".repeat(column) + text.slice(k);
|
|
341
|
+
}
|
|
342
|
+
function normalizeLabel(label) {
|
|
343
|
+
return label.trim().replace(/\s+/g, " ").toLowerCase();
|
|
344
|
+
}
|
|
345
|
+
function blocksText(blocks) {
|
|
346
|
+
return blocks
|
|
347
|
+
.map(block => {
|
|
348
|
+
switch (block.type) {
|
|
349
|
+
case "heading":
|
|
350
|
+
case "paragraph":
|
|
351
|
+
return inlineText(block.children);
|
|
352
|
+
case "code":
|
|
353
|
+
return block.text;
|
|
354
|
+
case "quote":
|
|
355
|
+
return blocksText(block.children);
|
|
356
|
+
case "list":
|
|
357
|
+
return block.items.map(item => blocksText(item.children)).join("\n");
|
|
358
|
+
case "table":
|
|
359
|
+
return [block.head, ...block.rows].map(row => row.map(cell => inlineText(cell.children)).join(" ")).join("\n");
|
|
360
|
+
case "rule":
|
|
361
|
+
return "";
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
.filter(Boolean)
|
|
365
|
+
.join("\n");
|
|
366
|
+
}
|
|
367
|
+
const PLAIN = /[^\\`<![\]*_~\n&hw]+/y;
|
|
368
|
+
const ESCAPABLE = /^[!-/:-@[-`{-~]$/;
|
|
369
|
+
const AUTOLINK = /<([a-zA-Z][a-zA-Z0-9+.-]{1,31}:[^\s<>]*)>/y;
|
|
370
|
+
const EMAIL_AUTOLINK = /<([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/y;
|
|
371
|
+
const BARE_URL = /(?:https?:\/\/|www\.)[^\s<]*/y;
|
|
372
|
+
const ENTITY = /&(?:#[xX]([0-9a-fA-F]{1,6})|#(\d{1,7})|([a-zA-Z][a-zA-Z0-9]{1,31}));/y;
|
|
373
|
+
const LABEL = /\[((?:[^[\]\\]|\\.){0,999})\]/y;
|
|
374
|
+
const SPACE = /\s/;
|
|
375
|
+
const PUNCTUATION = /[\p{P}\p{S}]/u;
|
|
376
|
+
function parseInline(source, context) {
|
|
377
|
+
const nodes = [];
|
|
378
|
+
const delimiters = [];
|
|
379
|
+
const brackets = [];
|
|
380
|
+
let text = "";
|
|
381
|
+
let i = 0;
|
|
382
|
+
const flush = () => {
|
|
383
|
+
if (text)
|
|
384
|
+
nodes.push({ type: "text", text });
|
|
385
|
+
text = "";
|
|
386
|
+
};
|
|
387
|
+
const push = (node) => {
|
|
388
|
+
flush();
|
|
389
|
+
nodes.push(node);
|
|
390
|
+
};
|
|
391
|
+
while (i < source.length) {
|
|
392
|
+
PLAIN.lastIndex = i;
|
|
393
|
+
const plain = PLAIN.exec(source);
|
|
394
|
+
if (plain) {
|
|
395
|
+
text += plain[0];
|
|
396
|
+
i += plain[0].length;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const char = source[i];
|
|
400
|
+
switch (char) {
|
|
401
|
+
case "\\": {
|
|
402
|
+
const next = source[i + 1] ?? "";
|
|
403
|
+
if (next === "\n") {
|
|
404
|
+
text = text.replace(/ +$/, "");
|
|
405
|
+
push({ type: "break" });
|
|
406
|
+
i = skipSpaces(source, i + 2);
|
|
407
|
+
}
|
|
408
|
+
else if (ESCAPABLE.test(next)) {
|
|
409
|
+
text += next;
|
|
410
|
+
i += 2;
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
text += char;
|
|
414
|
+
i++;
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case "\n": {
|
|
419
|
+
// Two spaces before a line break always break the line; without them, only with `breaks`.
|
|
420
|
+
const hard = / {2,}$/.test(text);
|
|
421
|
+
text = text.replace(/ +$/, "");
|
|
422
|
+
if (hard || context.breaks)
|
|
423
|
+
push({ type: "break" });
|
|
424
|
+
else
|
|
425
|
+
text += "\n";
|
|
426
|
+
i = skipSpaces(source, i + 1);
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
case "`": {
|
|
430
|
+
const run = runLength(source, i, char);
|
|
431
|
+
const close = findBacktickRun(source, i + run, run);
|
|
432
|
+
if (close < 0) {
|
|
433
|
+
text += source.slice(i, i + run);
|
|
434
|
+
i += run;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
let code = source.slice(i + run, close).replace(/\n/g, " ");
|
|
438
|
+
if (code.startsWith(" ") && code.endsWith(" ") && /[^ ]/.test(code))
|
|
439
|
+
code = code.slice(1, -1);
|
|
440
|
+
push({ type: "code", text: code });
|
|
441
|
+
i = close + run;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
case "<": {
|
|
445
|
+
AUTOLINK.lastIndex = i;
|
|
446
|
+
EMAIL_AUTOLINK.lastIndex = i;
|
|
447
|
+
const url = AUTOLINK.exec(source);
|
|
448
|
+
const email = url ? null : EMAIL_AUTOLINK.exec(source);
|
|
449
|
+
const found = url ?? email;
|
|
450
|
+
if (found) {
|
|
451
|
+
push({ type: "link", href: safeUrl(email ? `mailto:${found[1]}` : found[1]), children: [{ type: "text", text: found[1] }] });
|
|
452
|
+
i += found[0].length;
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
text += char;
|
|
456
|
+
i++;
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case "!":
|
|
461
|
+
case "[": {
|
|
462
|
+
const image = char === "!";
|
|
463
|
+
if (image && source[i + 1] !== "[") {
|
|
464
|
+
text += char;
|
|
465
|
+
i++;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
flush();
|
|
469
|
+
const node = { type: "text", text: image ? "![" : "[" };
|
|
470
|
+
nodes.push(node);
|
|
471
|
+
i += image ? 2 : 1;
|
|
472
|
+
brackets.push({ node, image, active: true, bottom: delimiters.length, labelStart: i });
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
case "]": {
|
|
476
|
+
flush();
|
|
477
|
+
const bracket = brackets.pop();
|
|
478
|
+
const target = bracket?.active ? linkTarget(source, i + 1, context.refs, source.slice(bracket.labelStart, i)) : null;
|
|
479
|
+
if (!bracket || !target) {
|
|
480
|
+
text += char;
|
|
481
|
+
i++;
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
const children = nodes.splice(nodes.indexOf(bracket.node) + 1);
|
|
485
|
+
nodes.pop();
|
|
486
|
+
processEmphasis(children, delimiters, bracket.bottom);
|
|
487
|
+
delimiters.length = bracket.bottom;
|
|
488
|
+
if (bracket.image) {
|
|
489
|
+
nodes.push({ type: "image", src: safeUrl(target.href, "image"), alt: inlineText(children), title: target.title });
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
nodes.push({ type: "link", href: safeUrl(target.href), title: target.title, children: mergeText(children) });
|
|
493
|
+
for (const open of brackets)
|
|
494
|
+
if (!open.image)
|
|
495
|
+
open.active = false;
|
|
496
|
+
}
|
|
497
|
+
i = target.end;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case "*":
|
|
501
|
+
case "_":
|
|
502
|
+
case "~": {
|
|
503
|
+
const run = runLength(source, i, char);
|
|
504
|
+
// Strikethrough takes exactly two tildes, so "~20 min" and "~~~" stay text.
|
|
505
|
+
if (char === "~" && run !== 2) {
|
|
506
|
+
text += source.slice(i, i + run);
|
|
507
|
+
i += run;
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
flush();
|
|
511
|
+
const node = { type: "text", text: source.slice(i, i + run) };
|
|
512
|
+
nodes.push(node);
|
|
513
|
+
const { canOpen, canClose } = flanking(char, source[i - 1] ?? " ", source[i + run] ?? " ");
|
|
514
|
+
if (canOpen || canClose)
|
|
515
|
+
delimiters.push({ char, node, count: run, length: run, canOpen, canClose, active: true });
|
|
516
|
+
i += run;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case "&": {
|
|
520
|
+
ENTITY.lastIndex = i;
|
|
521
|
+
const entity = ENTITY.exec(source);
|
|
522
|
+
const decoded = entity ? decodeEntity(entity[1], entity[2], entity[3]) : null;
|
|
523
|
+
if (decoded === null) {
|
|
524
|
+
text += char;
|
|
525
|
+
i++;
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
text += decoded;
|
|
529
|
+
i += entity[0].length;
|
|
530
|
+
}
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case "h":
|
|
534
|
+
case "w": {
|
|
535
|
+
// A bare URL starts a word, and never inside a link's text.
|
|
536
|
+
const before = source[i - 1];
|
|
537
|
+
const url = before === undefined || /[\s*_~(]/.test(before) ? bareUrl(source, i) : null;
|
|
538
|
+
if (url && !brackets.some(open => open.active && !open.image)) {
|
|
539
|
+
push({ type: "link", href: safeUrl(url.startsWith("www.") ? `http://${url}` : url), children: [{ type: "text", text: url }] });
|
|
540
|
+
i += url.length;
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
text += char;
|
|
544
|
+
i++;
|
|
545
|
+
}
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
default:
|
|
549
|
+
text += char;
|
|
550
|
+
i++;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
flush();
|
|
554
|
+
processEmphasis(nodes, delimiters, 0);
|
|
555
|
+
return mergeText(nodes);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Pairs emphasis delimiters above `bottom`, as CommonMark describes: each closer takes the nearest opener of the same
|
|
559
|
+
* character, `**` makes bold and `*` italic, and runs whose lengths add up to a multiple of three do not pair when
|
|
560
|
+
* one of them could both open and close.
|
|
561
|
+
*/
|
|
562
|
+
function processEmphasis(nodes, delimiters, bottom) {
|
|
563
|
+
// Where the search for an opener stopped last time, by closer kind, so text full of unmatched runs stays linear.
|
|
564
|
+
const openersBottom = new Map();
|
|
565
|
+
for (let c = bottom; c < delimiters.length; c++) {
|
|
566
|
+
const closer = delimiters[c];
|
|
567
|
+
while (closer.active && closer.canClose) {
|
|
568
|
+
const key = `${closer.char}${closer.canOpen ? 1 : 0}${closer.length % 3}`;
|
|
569
|
+
const floor = Math.max(bottom, openersBottom.get(key) ?? bottom);
|
|
570
|
+
let o = c - 1;
|
|
571
|
+
let opener;
|
|
572
|
+
for (; o >= floor; o--) {
|
|
573
|
+
const candidate = delimiters[o];
|
|
574
|
+
if (!candidate.active || candidate.char !== closer.char || !candidate.canOpen)
|
|
575
|
+
continue;
|
|
576
|
+
const bothWays = candidate.canClose || closer.canOpen;
|
|
577
|
+
if (bothWays && (candidate.length + closer.length) % 3 === 0 && (candidate.length % 3 !== 0 || closer.length % 3 !== 0))
|
|
578
|
+
continue;
|
|
579
|
+
opener = candidate;
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
if (!opener) {
|
|
583
|
+
openersBottom.set(key, c);
|
|
584
|
+
if (!closer.canOpen)
|
|
585
|
+
closer.active = false;
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
const use = closer.count >= 2 && opener.count >= 2 ? 2 : 1;
|
|
589
|
+
opener.count -= use;
|
|
590
|
+
closer.count -= use;
|
|
591
|
+
opener.node.text = opener.node.text.slice(use);
|
|
592
|
+
closer.node.text = closer.node.text.slice(use);
|
|
593
|
+
const start = nodes.indexOf(opener.node);
|
|
594
|
+
const end = nodes.indexOf(closer.node);
|
|
595
|
+
const children = mergeText(nodes.splice(start + 1, end - start - 1));
|
|
596
|
+
nodes.splice(start + 1, 0, { type: closer.char === "~" ? "strike" : use === 2 ? "strong" : "emphasis", children });
|
|
597
|
+
for (let k = o + 1; k < c; k++)
|
|
598
|
+
delimiters[k].active = false;
|
|
599
|
+
if (opener.count === 0) {
|
|
600
|
+
opener.active = false;
|
|
601
|
+
nodes.splice(nodes.indexOf(opener.node), 1);
|
|
602
|
+
}
|
|
603
|
+
if (closer.count === 0) {
|
|
604
|
+
closer.active = false;
|
|
605
|
+
nodes.splice(nodes.indexOf(closer.node), 1);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/** Whether a run of `char` between `before` and `after` can open or close emphasis. */
|
|
611
|
+
function flanking(char, before, after) {
|
|
612
|
+
const spaceBefore = SPACE.test(before);
|
|
613
|
+
const spaceAfter = SPACE.test(after);
|
|
614
|
+
const punctuationBefore = PUNCTUATION.test(before);
|
|
615
|
+
const punctuationAfter = PUNCTUATION.test(after);
|
|
616
|
+
const left = !spaceAfter && (!punctuationAfter || spaceBefore || punctuationBefore);
|
|
617
|
+
const right = !spaceBefore && (!punctuationBefore || spaceAfter || punctuationAfter);
|
|
618
|
+
// An underscore inside a word (snake_case_name) is not emphasis.
|
|
619
|
+
if (char === "_")
|
|
620
|
+
return { canOpen: left && (!right || punctuationBefore), canClose: right && (!left || punctuationAfter) };
|
|
621
|
+
return { canOpen: left, canClose: right };
|
|
622
|
+
}
|
|
623
|
+
/** The address and title after a link's text: `(url "title")`, `[label]`, `[]`, or nothing for `[label]` alone. */
|
|
624
|
+
function linkTarget(source, at, refs, label) {
|
|
625
|
+
if (source[at] === "(") {
|
|
626
|
+
const inline = inlineTarget(source, at);
|
|
627
|
+
if (inline)
|
|
628
|
+
return inline;
|
|
629
|
+
}
|
|
630
|
+
LABEL.lastIndex = at;
|
|
631
|
+
const full = LABEL.exec(source);
|
|
632
|
+
const key = normalizeLabel(full?.[1]?.trim() ? full[1] : label);
|
|
633
|
+
const reference = key ? refs.get(key) : undefined;
|
|
634
|
+
return reference ? { ...reference, end: full ? at + full[0].length : at } : null;
|
|
635
|
+
}
|
|
636
|
+
function inlineTarget(source, at) {
|
|
637
|
+
let i = skipWhitespace(source, at + 1);
|
|
638
|
+
let href;
|
|
639
|
+
if (source[i] === "<") {
|
|
640
|
+
const close = source.slice(i + 1).search(/[<>\n]/);
|
|
641
|
+
if (close < 0 || source[i + 1 + close] !== ">")
|
|
642
|
+
return null;
|
|
643
|
+
href = source.slice(i + 1, i + 1 + close);
|
|
644
|
+
i += close + 2;
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
const start = i;
|
|
648
|
+
let depth = 0;
|
|
649
|
+
for (; i < source.length; i++) {
|
|
650
|
+
const char = source[i];
|
|
651
|
+
if (char === "\\" && ESCAPABLE.test(source[i + 1] ?? ""))
|
|
652
|
+
i++;
|
|
653
|
+
else if (char === "(")
|
|
654
|
+
depth++;
|
|
655
|
+
else if (char === ")" && depth === 0)
|
|
656
|
+
break;
|
|
657
|
+
else if (char === ")")
|
|
658
|
+
depth--;
|
|
659
|
+
else if (char <= " ")
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
if (depth !== 0)
|
|
663
|
+
return null;
|
|
664
|
+
href = source.slice(start, i);
|
|
665
|
+
}
|
|
666
|
+
const afterHref = i;
|
|
667
|
+
i = skipWhitespace(source, i);
|
|
668
|
+
let title;
|
|
669
|
+
const quote = source[i];
|
|
670
|
+
if (i > afterHref && (quote === '"' || quote === "'" || quote === "(")) {
|
|
671
|
+
const close = quote === "(" ? ")" : quote;
|
|
672
|
+
let end = i + 1;
|
|
673
|
+
while (end < source.length && source[end] !== close)
|
|
674
|
+
end += source[end] === "\\" ? 2 : 1;
|
|
675
|
+
if (end >= source.length)
|
|
676
|
+
return null;
|
|
677
|
+
title = unescapeText(source.slice(i + 1, end));
|
|
678
|
+
i = skipWhitespace(source, end + 1);
|
|
679
|
+
}
|
|
680
|
+
return source[i] === ")" ? { href: unescapeText(href), title, end: i + 1 } : null;
|
|
681
|
+
}
|
|
682
|
+
function bareUrl(source, at) {
|
|
683
|
+
BARE_URL.lastIndex = at;
|
|
684
|
+
let url = BARE_URL.exec(source)?.[0];
|
|
685
|
+
if (!url)
|
|
686
|
+
return null;
|
|
687
|
+
// Punctuation at the end belongs to the sentence, and so does a closing parenthesis that opened before the URL.
|
|
688
|
+
for (;;) {
|
|
689
|
+
const last = url.slice(-1);
|
|
690
|
+
if (/[?!.,:;*_~'"]/.test(last))
|
|
691
|
+
url = url.slice(0, -1);
|
|
692
|
+
else if (last === ")" && url.split(")").length > url.split("(").length)
|
|
693
|
+
url = url.slice(0, -1);
|
|
694
|
+
else
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
return /^(?:https?:\/\/|www\.)[\p{L}\p{N}_-]/u.test(url) ? url : null;
|
|
698
|
+
}
|
|
699
|
+
function findBacktickRun(source, from, length) {
|
|
700
|
+
let at = source.indexOf("`", from);
|
|
701
|
+
while (at >= 0) {
|
|
702
|
+
const run = runLength(source, at, "`");
|
|
703
|
+
if (run === length)
|
|
704
|
+
return at;
|
|
705
|
+
at = source.indexOf("`", at + run);
|
|
706
|
+
}
|
|
707
|
+
return -1;
|
|
708
|
+
}
|
|
709
|
+
function runLength(source, at, char) {
|
|
710
|
+
let end = at;
|
|
711
|
+
while (source[end] === char)
|
|
712
|
+
end++;
|
|
713
|
+
return end - at;
|
|
714
|
+
}
|
|
715
|
+
function skipSpaces(source, at) {
|
|
716
|
+
while (source[at] === " ")
|
|
717
|
+
at++;
|
|
718
|
+
return at;
|
|
719
|
+
}
|
|
720
|
+
function skipWhitespace(source, at) {
|
|
721
|
+
while (source[at] === " " || source[at] === "\t" || source[at] === "\n")
|
|
722
|
+
at++;
|
|
723
|
+
return at;
|
|
724
|
+
}
|
|
725
|
+
function mergeText(nodes) {
|
|
726
|
+
const merged = [];
|
|
727
|
+
for (const node of nodes) {
|
|
728
|
+
const last = merged[merged.length - 1];
|
|
729
|
+
if (node.type !== "text")
|
|
730
|
+
merged.push(node);
|
|
731
|
+
else if (!node.text)
|
|
732
|
+
continue;
|
|
733
|
+
else if (last?.type === "text")
|
|
734
|
+
merged[merged.length - 1] = { type: "text", text: last.text + node.text };
|
|
735
|
+
else
|
|
736
|
+
merged.push({ type: "text", text: node.text });
|
|
737
|
+
}
|
|
738
|
+
return merged;
|
|
739
|
+
}
|
|
740
|
+
function inlineText(nodes) {
|
|
741
|
+
return nodes
|
|
742
|
+
.map(node => {
|
|
743
|
+
switch (node.type) {
|
|
744
|
+
case "text":
|
|
745
|
+
case "code":
|
|
746
|
+
return node.text;
|
|
747
|
+
case "image":
|
|
748
|
+
return node.alt;
|
|
749
|
+
case "break":
|
|
750
|
+
return "\n";
|
|
751
|
+
default:
|
|
752
|
+
return inlineText(node.children);
|
|
753
|
+
}
|
|
754
|
+
})
|
|
755
|
+
.join("");
|
|
756
|
+
}
|
|
757
|
+
/** Code points of the named entities most often typed in text. Others stay as written. */
|
|
758
|
+
const NAMED_ENTITIES = new Map([
|
|
759
|
+
["amp", 38], ["lt", 60], ["gt", 62], ["quot", 34], ["apos", 39], ["nbsp", 160], ["copy", 169], ["reg", 174],
|
|
760
|
+
["trade", 8482], ["hellip", 8230], ["mdash", 8212], ["ndash", 8211], ["lsquo", 8216], ["rsquo", 8217],
|
|
761
|
+
["ldquo", 8220], ["rdquo", 8221], ["laquo", 171], ["raquo", 187], ["times", 215], ["divide", 247], ["deg", 176],
|
|
762
|
+
["plusmn", 177], ["middot", 183], ["bull", 8226], ["larr", 8592], ["rarr", 8594], ["uarr", 8593], ["darr", 8595],
|
|
763
|
+
["euro", 8364], ["pound", 163], ["yen", 165], ["cent", 162], ["sect", 167], ["para", 182],
|
|
764
|
+
]);
|
|
765
|
+
function decodeEntity(hex, decimal, name) {
|
|
766
|
+
const code = hex ? parseInt(hex, 16) : decimal ? parseInt(decimal, 10) : NAMED_ENTITIES.get(name ?? "");
|
|
767
|
+
if (code === undefined)
|
|
768
|
+
return null;
|
|
769
|
+
const valid = code > 0 && code <= 0x10ffff && (code < 0xd800 || code > 0xdfff);
|
|
770
|
+
return String.fromCodePoint(valid ? code : 0xfffd);
|
|
771
|
+
}
|
|
772
|
+
/** Backslash escapes and entities in an address, a title, or a code block's language. */
|
|
773
|
+
function unescapeText(text) {
|
|
774
|
+
return text.replace(/\\([!-/:-@[-`{-~])|&(?:#[xX]([0-9a-fA-F]{1,6})|#(\d{1,7})|([a-zA-Z][a-zA-Z0-9]{1,31}));/g, (whole, escaped, hex, decimal, name) => escaped ?? decodeEntity(hex, decimal, name) ?? whole);
|
|
775
|
+
}
|