diff --git a/package.json b/package.json index 9a112857..4b3db5e8 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "vue": "^3.3.8" }, "dependencies": { - "@kong/icons": "^1.8.7", + "@kong/icons": "^1.8.8", "@sindresorhus/slugify": "^2.2.1", "@vueuse/core": "^10.7.0", "html-format": "^1.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cb96988..495793f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@kong/icons': - specifier: ^1.8.7 - version: 1.8.7(vue@3.3.11) + specifier: ^1.8.8 + version: 1.8.8(vue@3.3.11) '@sindresorhus/slugify': specifier: ^2.2.1 version: 2.2.1 @@ -823,8 +823,8 @@ packages: resolution: {integrity: sha512-I6C2HHJ+Kx14Exmr9OEe4yzHvhneC5RGrWzViZXcWNOYCl2VN9dMmgjTAaowbdeyLEeZ+4AtC+LJ2JU7eN4o8w==} dev: true - /@kong/icons@1.8.7(vue@3.3.11): - resolution: {integrity: sha512-gSs8VRUyPVacVg/4/bTjnRSuIzcXrWFvV7+4vxjUJXt0dMWBq8GzMZ4JTj9ar7eUHLWeY1nmP55PmwF2Qwsa5A==} + /@kong/icons@1.8.8(vue@3.3.11): + resolution: {integrity: sha512-TFBSTxnjmot6IBmHNpGcpJreB/ki5gi6GUY2Oy/snLdIkV3r50jnniz5w/DhA2+PsmjUsvwQ4hzbdyKqeXTZ3A==} engines: {node: '>=18.17.0'} peerDependencies: vue: ^3.3.11 diff --git a/sandbox/App.vue b/sandbox/App.vue index 6cbc77e4..107ebc03 100644 --- a/sandbox/App.vue +++ b/sandbox/App.vue @@ -7,6 +7,8 @@ { diff --git a/sandbox/mock-document-response.ts b/sandbox/mock-document-response.ts index 3f71db55..1117f815 100644 --- a/sandbox/mock-document-response.ts +++ b/sandbox/mock-document-response.ts @@ -1,6 +1,6 @@ export default { id: 'da315607-4d04-4a54-810d-45156b575932', - content: "# Markdown UI\n\nA markdown renderer and edit UI.\n\n## Formatting types\n\n### Code block and inline code\n\n```typescript {2}\n// TODO: Fenced code blocks need default styles\nconst name = 'Adam'\n```\n\nThis sentence has \\`inlineCode\\` in the middle.\n\n### Emoji\n\n:smile: :rocket: :tada:\n\n### Link\n\n[link](https://github.com)\n\n### Tables\n\n| Column1 | Column2 | Column3 |\n| :--- | :--- | :--- |\n| Content | Content | Content |\n\n### Todo items\n\n- [x] Todo item checked\n- [ ] Another item not checked\n\n---\n\n### Diagrams\n\n#### Mermaid\n\n```mermaid\ngitGraph\n commit\n commit\n branch develop\n checkout develop\n commit\n commit\n checkout main\n merge develop\n commit\n commit\n```\n\n#### Plantuml\n\n```plantuml\nBob -> Alice : hello\n```\n\n#### DOT\n\n```dot\ndigraph example1 {\n 1 -> 2 -> { 4, 5 };\n 1 -> 3 -> { 6, 7 };\n}\n```\n", + content: "# Markdown UI\n\nA markdown renderer and edit UI.\n\n## Formatting types\n\n### Code block and inline code\n\n```typescript\n// TODO: Fenced code blocks need default styles\nconst name = 'Adam'\n```\n\nThis sentence has \\`inlineCode\\` in the middle.\n\n### Emoji\n\n:smile: :rocket: :tada:\n\n### Link\n\n[link](https://github.com)\n\n### Tables\n\n| Column1 | Column2 | Column3 |\n| :--- | :--- | :--- |\n| Content | Content | Content |\n\n### Todo items\n\n- [x] Todo item checked\n- [ ] Another item not checked\n\n---\n\n### Diagrams\n\n#### Mermaid\n\n```mermaid\ngitGraph\n commit\n commit\n branch develop\n checkout develop\n commit\n commit\n checkout main\n merge develop\n commit\n commit\n```\n\n#### Plantuml\n\n```plantuml\nBob -> Alice : hello\n```\n\n#### DOT\n\n```dot\ndigraph example1 {\n 1 -> 2 -> { 4, 5 };\n 1 -> 3 -> { 6, 7 };\n}\n```\n", parent_document_id: null, slug: 'markdown', title: 'markdown', diff --git a/src/assets/_mixins.scss b/src/assets/_mixins.scss new file mode 100644 index 00000000..234f39ef --- /dev/null +++ b/src/assets/_mixins.scss @@ -0,0 +1,57 @@ +@mixin icon-button { + align-items: center; + background-color: var(--kui-color-background-transparent, $kui-color-background-transparent); + border: var(--kui-border-width-20, $kui-border-width-20) solid + var(--kui-color-border-transparent, $kui-color-border-transparent); + border-radius: var(--kui-border-radius-30, $kui-border-radius-30); + color: var(--kui-color-text-neutral, $kui-color-text-neutral); + cursor: pointer; + display: inline-flex; + font-family: var(--kui-font-family-text, $kui-font-family-text); + font-size: var(--kui-font-size-30, $kui-font-size-30); + font-weight: var(--kui-font-weight-semibold, $kui-font-weight-semibold); + gap: var(--kui-space-30, $kui-space-30); + justify-content: center; + padding: var(--kui-space-10, $kui-space-10); + // Remove tap color highlight on mobile Safari + -webkit-tap-highlight-color: rgba(255, 255, 255, 0); + text-decoration: none; + transition: + background-color 0.2s ease-in-out, + color 0.2s ease-in-out, + border-color 0.2s ease-in-out; + user-select: none; + vertical-align: middle; + white-space: nowrap; + + &:hover:not(:disabled):not(:focus):not(:active) { + background-color: var(--kui-color-background-neutral-weaker, $kui-color-background-neutral-weaker); + } + + &:focus { + background-color: var(--kui-color-background-neutral-weaker, $kui-color-background-neutral-weaker); + } + + &:active { + background-color: var(--kui-color-background-neutral-weak, $kui-color-background-neutral-weak); + } + + &:disabled, + &[disabled] { + background-color: var(--kui-color-background-disabled, $kui-color-background-disabled); + box-shadow: none; + color: var(--kui-color-text-disabled, $kui-color-text-disabled); + cursor: not-allowed; + } + + &:focus, + &:active, + &:focus-visible { + outline: none; + } + + &:focus-visible { + // Same as $kui-shadow-focus with 2px instead of 4px + box-shadow: 0px 0px 0px 2px rgba(0, 68, 244, 0.2); + } +} diff --git a/src/components/MarkdownContent.vue b/src/components/MarkdownContent.vue index bfd570b9..7b82dc83 100644 --- a/src/components/MarkdownContent.vue +++ b/src/components/MarkdownContent.vue @@ -25,6 +25,8 @@ watch(() => props.content, (content: string): void => { diff --git a/src/components/toolbar/InfoTooltip.vue b/src/components/toolbar/InfoTooltip.vue index 6ef12b8a..582f0357 100644 --- a/src/components/toolbar/InfoTooltip.vue +++ b/src/components/toolbar/InfoTooltip.vue @@ -39,23 +39,24 @@ defineProps({ } .tooltip-content { - background: $kui-color-background-inverse; - border-radius: $kui-border-radius-20; + background: var(--kui-color-background-inverse, $kui-color-background-inverse); + border-radius: var(--kui-border-radius-20, $kui-border-radius-20); box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - color: $kui-color-text-inverse; - font-family: $kui-font-family-text; - font-size: $kui-font-size-20; - font-weight: $kui-font-weight-regular; + color: var(--kui-color-text-inverse, $kui-color-text-inverse); + font-family: var(--kui-font-family-text, $kui-font-family-text); + font-size: var(--kui-font-size-20, $kui-font-size-20); + font-weight: var(--kui-font-weight-regular, $kui-font-weight-regular); left: 50%; - line-height: $kui-line-height-20; + line-height: var(--kui-line-height-20, $kui-line-height-20); max-width: 200px; opacity: 0; - padding: $kui-space-40; + padding: var(--kui-space-40, $kui-space-40); pointer-events: none; position: absolute; - top: calc(100% + 6px); + top: calc(100% + 4px); transform: translateX(-50%); transition: opacity 0.2s ease-in-out; width: max-content; + z-index: 1; } diff --git a/src/components/toolbar/MarkdownToolbar.vue b/src/components/toolbar/MarkdownToolbar.vue index 856cc429..9be30fa9 100644 --- a/src/components/toolbar/MarkdownToolbar.vue +++ b/src/components/toolbar/MarkdownToolbar.vue @@ -176,7 +176,7 @@ import type { MarkdownMode, FormatOption, TemplateOption, InlineFormat, Markdown import IconButton from '@/components/toolbar/IconButton.vue' import InfoTooltip from '@/components/toolbar/InfoTooltip.vue' import TooltipShortcut from '@/components/toolbar/TooltipShortcut.vue' -import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, SubscriptIcon, SuperscriptIcon, MarkIcon, CodeIcon, CodeblockIcon, TableIcon, TasklistIcon, ListUnorderedIcon, MarkdownIcon, HtmlIcon, BlockquoteIcon, ExpandIcon, CollapseIcon } from '@kong/icons' +import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, SubscriptIcon, SuperscriptIcon, MarkIcon, CodeIcon, CodeblockIcon, TableIcon, TasklistIcon, ListUnorderedIcon, ListOrderedIcon, MarkdownIcon, HtmlIcon, BlockquoteIcon, ExpandIcon, CollapseIcon } from '@kong/icons' const mode: Ref = inject(MODE_INJECTION_KEY, ref('read')) const editable: Ref = inject(EDITABLE_INJECTION_KEY, ref(false)) @@ -242,10 +242,11 @@ const formatOptions: FormatOption[] = [ ] const templateOptions: TemplateOption[] = [ - { label: 'CodeBlock', action: 'codeblock', icon: CodeblockIcon }, + { label: 'Codeblock', action: 'codeblock', icon: CodeblockIcon }, { label: 'Table', action: 'table', icon: TableIcon }, - { label: 'Task', action: 'task', icon: TasklistIcon }, - { label: 'UL', action: 'unordered-list', icon: ListUnorderedIcon }, + { label: 'Tasklist', action: 'task', icon: TasklistIcon }, + { label: 'Unordered List', action: 'unordered-list', icon: ListUnorderedIcon }, + { label: 'Ordered List', action: 'ordered-list', icon: ListOrderedIcon }, { label: 'Blockquote', action: 'blockquote', icon: BlockquoteIcon }, ] @@ -261,7 +262,7 @@ onMounted(() => { align-items: center; background-color: var(--kui-color-background, $kui-color-background); display: flex; - gap: $kui-space-70; + gap: var(--kui-space-70, $kui-space-70); height: v-bind('TOOLBAR_HEIGHT'); justify-content: space-between; // overflow-x: auto; // TODO: Handle overflow @@ -274,7 +275,8 @@ onMounted(() => { button { border: 0; border-right: 1px solid $kui-color-border; - padding: $kui-space-20 $kui-space-30; + cursor: pointer; + padding: var(--kui-space-20, $kui-space-20) var(--kui-space-30, $kui-space-30); &:disabled { cursor: not-allowed; @@ -295,14 +297,14 @@ onMounted(() => { .toolbar-right { align-items: center; display: flex; - gap: $kui-space-20; - padding: $kui-space-40 $kui-space-0; + gap: var(--kui-space-20, $kui-space-20); + padding: var(--kui-space-40, $kui-space-40) var(--kui-space-0, $kui-space-0); } .toolbar-divider { - background-color: $kui-color-border; + background-color: var(--kui-color-border, $kui-color-border); height: 16px; - margin: 0 $kui-space-20; + margin: var(--kui-space-0, $kui-space-0) var(--kui-space-20, $kui-space-20); width: 2px; } diff --git a/src/components/toolbar/TooltipShortcut.vue b/src/components/toolbar/TooltipShortcut.vue index 291fd3bf..091dba41 100644 --- a/src/components/toolbar/TooltipShortcut.vue +++ b/src/components/toolbar/TooltipShortcut.vue @@ -55,18 +55,18 @@ onMounted(() => { align-items: center; display: flex; flex-direction: column; - font-family: $kui-font-family-text; - font-size: $kui-font-size-20; - font-weight: $kui-font-weight-regular; - gap: $kui-space-20; + font-family: var(--kui-font-family-text, $kui-font-family-text); + font-size: var(--kui-font-size-20, $kui-font-size-20); + font-weight: var(--kui-font-weight-regular, $kui-font-weight-regular); + gap: var(--kui-space-30, $kui-space-30); justify-content: center; user-select: none; .keys { align-items: center; display: flex; - font-family: $kui-font-family-code; - gap: $kui-space-20; + font-family: var(--kui-font-family-code, $kui-font-family-code); + gap: var(--kui-space-20, $kui-space-20); justify-content: center; min-width: auto; white-space: nowrap; @@ -74,18 +74,19 @@ onMounted(() => { .keyboard-button { background: rgba(255, 255, 255, 0.1); - border: $kui-border-width-10 solid $kui-color-border; - border-radius: $kui-border-radius-20; + border: var(--kui-border-width-10, $kui-border-width-10) solid var(--kui-color-border, $kui-color-border); + border-radius: var(--kui-border-radius-20, $kui-border-radius-20); display: block; line-height: 1; min-width: 0; - padding: $kui-space-20 $kui-space-30; + padding: var(--kui-space-20, $kui-space-20) var(--kui-space-30, $kui-space-30); position: relative; top: -1px; width: auto; } .meta-key { + // Control &:after { content: 'Ctrl'; } diff --git a/src/composables/useMarkdownActions.ts b/src/composables/useMarkdownActions.ts index 76a676ca..db0b77ed 100644 --- a/src/composables/useMarkdownActions.ts +++ b/src/composables/useMarkdownActions.ts @@ -1,6 +1,6 @@ import { reactive, nextTick } from 'vue' import type { Ref } from 'vue' -import { InlineFormatWrapper, DEFAULT_CODEBLOCK_LANGUAGE, MARKDOWN_TEMPLATE_CODEBLOCK, MARKDOWN_TEMPLATE_TASK, MARKDOWN_TEMPLATE_UL, MARKDOWN_TEMPLATE_BLOCKQUOTE, MARKDOWN_TEMPLATE_TABLE } from '@/constants' +import { InlineFormatWrapper, DEFAULT_CODEBLOCK_LANGUAGE, MARKDOWN_TEMPLATE_CODEBLOCK, MARKDOWN_TEMPLATE_TASK, MARKDOWN_TEMPLATE_UL, MARKDOWN_TEMPLATE_OL, MARKDOWN_TEMPLATE_BLOCKQUOTE, MARKDOWN_TEMPLATE_TABLE, NEW_LINE_CHARACTER } from '@/constants' import type { InlineFormat, MarkdownTemplate } from '@/types' /** @@ -221,8 +221,8 @@ export default function useMarkdownActions( const startText = rawMarkdown.value.substring(0, selectedText.start) - // When removing tabs, ensure string starts with two spaces; if not, exit - if (action === 'remove' && !startText.endsWith(spaces)) { + // When removing tabs, ensure string starts with two spaces; or a list item that is already indented. If not, exit + if (action === 'remove' && (!startText.endsWith(spaces) && !startText.endsWith(' ' + MARKDOWN_TEMPLATE_UL))) { return } @@ -231,9 +231,9 @@ export default function useMarkdownActions( lineBreakCount = (selectedText.text.match(/\n(?!\n)/g) || []).length // If text is selected if (action === 'add') { - rawMarkdown.value = startText + spaces + selectedText.text.replace(/\n(?!\n)/g, `\n${spaces}`) + rawMarkdown.value.substring(selectedText.end) + rawMarkdown.value = startText + spaces + selectedText.text.replace(/\n(?!\n)/g, `${NEW_LINE_CHARACTER}${spaces}`) + rawMarkdown.value.substring(selectedText.end) } else { - rawMarkdown.value = rawMarkdown.value.substring(0, selectedText.start - spaces.length) + selectedText.text.replaceAll(`\n${spaces}`, '\n') + rawMarkdown.value.substring(selectedText.end) + rawMarkdown.value = rawMarkdown.value.substring(0, selectedText.start - spaces.length) + selectedText.text.replaceAll(`${NEW_LINE_CHARACTER}${spaces}`, NEW_LINE_CHARACTER) + rawMarkdown.value.substring(selectedText.end) } } else { // If text is not selected @@ -241,6 +241,8 @@ export default function useMarkdownActions( // If text starts with an inline template if (startText.endsWith(MARKDOWN_TEMPLATE_UL)) { rawMarkdown.value = action === 'add' ? rawMarkdown.value.substring(0, selectedText.start - MARKDOWN_TEMPLATE_UL.length) + spaces + MARKDOWN_TEMPLATE_UL + rawMarkdown.value.substring(selectedText.end) : rawMarkdown.value.substring(0, selectedText.start - spaces.length - MARKDOWN_TEMPLATE_UL.length) + MARKDOWN_TEMPLATE_UL + rawMarkdown.value.substring(selectedText.end) + } else if (startText.endsWith(MARKDOWN_TEMPLATE_OL)) { + rawMarkdown.value = action === 'add' ? rawMarkdown.value.substring(0, selectedText.start - MARKDOWN_TEMPLATE_OL.length) + spaces + MARKDOWN_TEMPLATE_OL + rawMarkdown.value.substring(selectedText.end) : rawMarkdown.value.substring(0, selectedText.start - spaces.length - MARKDOWN_TEMPLATE_OL.length) + MARKDOWN_TEMPLATE_OL + rawMarkdown.value.substring(selectedText.end) } else { rawMarkdown.value = action === 'add' ? startText + spaces + rawMarkdown.value.substring(selectedText.end) : rawMarkdown.value.substring(0, selectedText.start - spaces.length) + rawMarkdown.value.substring(selectedText.end) } @@ -266,7 +268,15 @@ export default function useMarkdownActions( } /** Check if the single line template already exists on the line */ - const singleLineTemplateExists = (startText: string, template: string) => String(startText?.split('\n')?.pop() || '').endsWith(template) + const singleLineTemplateExists = (startText: string, template: string) => { + // Special handling for ordered list items + if (template === MARKDOWN_TEMPLATE_OL) { + // If the startText begins with `{number}. ` + return /^\d{1,}\. /.test(String(startText?.split(NEW_LINE_CHARACTER)?.pop() || '')) + } + // All other templates + return String(startText?.split(NEW_LINE_CHARACTER)?.pop() || '').endsWith(template) + } /** * Insert a markdown template at the current cursor position. @@ -291,7 +301,7 @@ export default function useMarkdownActions( const startText = rawMarkdown.value.substring(0, selectedText.start) // If the previous line is not empty and doesn't already have an empty line above it (if so, empty string) // If the line does not start with a new line, insert two, otherwise, insert one new line - const needsNewLine: string = startText.length === 0 || startText.endsWith('\n\n') ? '' : /(.*)?[^\n]$/.test(startText) ? '\n\n' : '\n' + const needsNewLine: string = startText.length === 0 || startText.endsWith(`${NEW_LINE_CHARACTER}${NEW_LINE_CHARACTER}`) ? '' : /(.*)?[^\n]$/.test(startText) ? `${NEW_LINE_CHARACTER}${NEW_LINE_CHARACTER}` : NEW_LINE_CHARACTER let markdownTemplate: string = '' @@ -318,6 +328,17 @@ export default function useMarkdownActions( needsNewLine + MARKDOWN_TEMPLATE_UL break + case 'ordered-list': + // Do nothing if the template already exists + if (singleLineTemplateExists(startText, MARKDOWN_TEMPLATE_OL)) { + await focusTextarea() + return + } + // needsNewLine not needed here + markdownTemplate = + needsNewLine + + MARKDOWN_TEMPLATE_OL + break case 'blockquote': // Do nothing if the template already exists if (singleLineTemplateExists(startText, MARKDOWN_TEMPLATE_BLOCKQUOTE)) { @@ -388,10 +409,9 @@ export default function useMarkdownActions( // Check the current line to see if we're within another format block (e.g. list, code, etc.) const startText = rawMarkdown.value.substring(0, selectedText.start) // Grab the last line before the cursor - const lastLine = startText?.split('\n')?.pop() || '' + const lastLine = startText?.split(NEW_LINE_CHARACTER)?.pop() || '' - const newLineCharacter = '\n' - let newLineContent = newLineCharacter + let newLineContent = NEW_LINE_CHARACTER // Should we remove the new line template on second Enter keypress let removeNewLineTemplate = false @@ -400,23 +420,51 @@ export default function useMarkdownActions( const newLineTemplates = [ MARKDOWN_TEMPLATE_TASK, // Task template **must** be processed before UL template since they share starting chars MARKDOWN_TEMPLATE_UL, + MARKDOWN_TEMPLATE_OL, MARKDOWN_TEMPLATE_BLOCKQUOTE, ] // Loop through the new line templates. // If the last line before the \n starts with any formatting templates, also inject the template into the next line for (const template of newLineTemplates) { - if (lastLine.trimStart().startsWith(template)) { - templateLength = template.length - // If the last task item is empty, remove the template instead - if (lastLine.trimStart() === template) { - removeNewLineTemplate = true - } else { - // Add a new line appended with the same template with indentation, if applicable - newLineContent += lastLine.split(template)[0] + template + // Special handling for Ordered Lists + if (template === MARKDOWN_TEMPLATE_OL) { + if (/^\d{1,}\. /.test(lastLine.trimStart())) { + // Remove the `1` in the ordered list template + const numberSuffix = MARKDOWN_TEMPLATE_OL.replace('1', '') + // Split the line by the `. ` string after the number, and get the first entry (which should be the list number) + const listNumber = Number(lastLine.trimStart().split(numberSuffix)[0]) + if (!isNaN(listNumber) && listNumber > 0) { + // Increment the number for the next list item + const newLineNumber: number = listNumber + 1 + // Get the template length + templateLength = String(newLineNumber + numberSuffix).length + + // If the last list item is empty, remove the template instead + if (/^\d{1,}\. $/.test(lastLine.trimStart())) { + removeNewLineTemplate = true + } else { + // Add a new line appended with the same template with indentation, if applicable + newLineContent += lastLine.split(listNumber + numberSuffix)[0] + newLineNumber + numberSuffix + } + } + // We found a match, so exit the loop + break + } + } else { + // All other templates (other than ordered list) + if (lastLine.trimStart().startsWith(template)) { + templateLength = template.length + // If the last list item is empty, remove the template instead + if (lastLine.trimStart() === template) { + removeNewLineTemplate = true + } else { + // Add a new line appended with the same template with indentation, if applicable + newLineContent += lastLine.split(template)[0] + template + } + // We found a match, so exit the loop + break } - // We found a match, so exit the loop - break } } diff --git a/src/composables/useMarkdownIt.ts b/src/composables/useMarkdownIt.ts index c537d950..8c013484 100644 --- a/src/composables/useMarkdownIt.ts +++ b/src/composables/useMarkdownIt.ts @@ -1,5 +1,6 @@ import { ref } from 'vue' import useShikiji from '@/composables/useShikiji' +import { NEW_LINE_CHARACTER, COPY_ICON_SVG, HEADER_LINK_ICON_SVG } from '@/constants' // markdown-it import MarkdownIt from 'markdown-it' @@ -43,7 +44,7 @@ export default function useMarkdownIt(theme: 'light' | 'dark' = 'light') { placement: 'before', class: 'header-anchor', // The class applied to the anchor tag; allows for styling // Utilize an SVG icon instead of a `#` string - symbol: '', + symbol: HEADER_LINK_ICON_SVG, }), }) .use(abbreviation) @@ -70,16 +71,17 @@ export default function useMarkdownIt(theme: 'light' | 'dark' = 'light') { md.value.linkify.set({ fuzzyLink: false }) // Customize table element - md.value.renderer.rules.table_open = () => '\n' + md.value.renderer.rules.table_open = () => '
' + NEW_LINE_CHARACTER - // Configure external links - const defaultLinkRenderer = md.value.renderer.rules.link_open || - function(tokens: Record[], idx: number, options: Record, env: any, self: Record) { + const getDefaultRenderer = (original: any): Function => { + return original || function(tokens: Record[], idx: number, options: Record, env: any, self: Record) { return self.renderToken(tokens, idx, options) } + } + // Configure custom external links + const defaultLinkRenderer = getDefaultRenderer(md.value.renderer.rules.link_open) const externalAnchorAttributes: Record = { target: '_blank' } - md.value.renderer.rules.link_open = (tokens: Record[], idx: number, options: Record, env: any, self: Record) => { Object.keys(externalAnchorAttributes).forEach((attribute: string) => { const aIndex = tokens[idx].attrIndex(attribute) @@ -94,6 +96,31 @@ export default function useMarkdownIt(theme: 'light' | 'dark' = 'light') { }) return defaultLinkRenderer(tokens, idx, options, env, self) } + + // Configure custom code blocks + const defaultCodeblockRenderer = getDefaultRenderer(md.value.renderer.rules.fence) + md.value.renderer.rules.fence = (tokens: Record[], idx: number, options: Record, env: any, self: Record) => { + // Strip out quote characters + const content = tokens[idx].content + .replaceAll('"', '"') + .replaceAll("'", ''') + const originalContent = defaultCodeblockRenderer(tokens, idx, options, env, self) + + if (content.length === 0) { + return originalContent + } + + // Styles injected from `src/components/MarkdownContent.vue` + // The event is bound to an element with `.kong-markdown-code-block-copy[data-copytext]` + return ` +
+ ${originalContent} + +
+ ` + } } return { diff --git a/src/composables/useShikiji.ts b/src/composables/useShikiji.ts index efb1ab87..4f7a8f11 100644 --- a/src/composables/useShikiji.ts +++ b/src/composables/useShikiji.ts @@ -7,7 +7,7 @@ export default function useShikiji() { const highlighter = await getHighlighterCore({ themes: [ import('shikiji/themes/github-light.mjs'), - import('shikiji/themes/github-dark.mjs'), + import('shikiji/themes/material-theme-palenight.mjs'), ], // TODO: For now, I'm including all languages but this bumps up the package size ~6MB langs: [ @@ -188,7 +188,7 @@ export default function useShikiji() { }) return fromHighlighter(highlighter, { - theme: theme === 'light' ? 'github-light' : 'github-dark', + theme: theme === 'light' ? 'github-light' : 'material-theme-palenight', }) } diff --git a/src/constants.ts b/src/constants.ts index 0e561787..f7899db2 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -17,16 +17,19 @@ export enum InlineFormatWrapper { /** The height of the .markdown-ui-toolbar */ export const TOOLBAR_HEIGHT: string = '40px' +/** The markdown new line character */ +export const NEW_LINE_CHARACTER = '\n' + /** The markdown template for a codeblock */ export const MARKDOWN_TEMPLATE_CODEBLOCK = -'```' + DEFAULT_CODEBLOCK_LANGUAGE + '\n' + -'\n' + +'```' + DEFAULT_CODEBLOCK_LANGUAGE + NEW_LINE_CHARACTER + +NEW_LINE_CHARACTER + '```' /** The markdown template for a table */ export const MARKDOWN_TEMPLATE_TABLE = -'| Column1 | Column2 | Column3 |\n' + -'| :--- | :--- | :--- |\n' + +'| Column1 | Column2 | Column3 |' + NEW_LINE_CHARACTER + +'| :--- | :--- | :--- |' + NEW_LINE_CHARACTER + '| Content | Content | Content |' /** The markdown template for a task. Ensure trailing space remains */ @@ -34,6 +37,14 @@ export const MARKDOWN_TEMPLATE_TASK = '- [ ] ' /** The markdown template for an unordered list. Ensure trailing space remains */ export const MARKDOWN_TEMPLATE_UL = '- ' +/** The markdown template for an ordered list. Ensure trailing space remains */ +export const MARKDOWN_TEMPLATE_OL = '1. ' /** The markdown template for a blockquote. Ensure trailing space remains */ export const MARKDOWN_TEMPLATE_BLOCKQUOTE = '> ' + +/** The inline SVG copy icon */ +export const COPY_ICON_SVG = '' + +/** The inline SVG to display as a header link */ +export const HEADER_LINK_ICON_SVG = '' diff --git a/src/global-components.d.ts b/src/global-components.d.ts deleted file mode 100644 index 2f0048d6..00000000 --- a/src/global-components.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Import globally available components -import '@kong/kongponents/dist/types/global-components' diff --git a/src/types/markdown-ui.ts b/src/types/markdown-ui.ts index 3f622145..845d74fd 100644 --- a/src/types/markdown-ui.ts +++ b/src/types/markdown-ui.ts @@ -32,6 +32,7 @@ export type MarkdownTemplate = | 'codeblock' | 'task' | 'unordered-list' + | 'ordered-list' | 'blockquote' export interface TemplateOption {