From 7a7b1bbc6d99e99a26f64a66e651c25b5da465cc Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Tue, 3 Jan 2023 19:25:20 +0100 Subject: [PATCH 1/4] enable find widget in the preview window --- src/tothom.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tothom.ts b/src/tothom.ts index 199346e..432bfea 100644 --- a/src/tothom.ts +++ b/src/tothom.ts @@ -29,6 +29,7 @@ export class Tothom { panel = vscode.window.createWebviewPanel(WEBVIEW_PANEL_TYPE, title, vscode.ViewColumn.Active, { enableScripts: true, + enableFindWidget: true, retainContextWhenHidden: true, localResourceRoots: [ this._extensionUri From a2cd70135b8966bf505c7fb5b6cb92b5be2d00f9 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Tue, 3 Jan 2023 20:28:04 +0100 Subject: [PATCH 2/4] add support for anchor links - support for the following slugify modes: azure, bitbucket, gitea, github, gitlab, vscode - implementation borrowed from https://github.com/yzhang-gh/vscode-markdown --- samples/tothom.md | 56 +++++++++++++ src/engine.ts | 38 +++++++++ src/slugify.ts | 200 ++++++++++++++++++++++++++++++++++++++++++++++ src/tothom.ts | 62 +++++++------- 4 files changed, 322 insertions(+), 34 deletions(-) create mode 100644 samples/tothom.md create mode 100644 src/engine.ts create mode 100644 src/slugify.ts diff --git a/samples/tothom.md b/samples/tothom.md new file mode 100644 index 0000000..c9e5e7f --- /dev/null +++ b/samples/tothom.md @@ -0,0 +1,56 @@ +# Tothom + +- [Hello World!](#hello-world) +- [Interpolation](#interpolation) +- [Multiline](#multiline) +- [Compatibility](#compatibility) + - [Unordered Lists](#unordered-lists) + - [Ordered Lists](#ordered-lists) + - [Task lists](#task-lists) + - [Links](#links) + +## Hello World! + +```sh +echo 'Hello World!' +``` + +## Interpolation + +```sh +echo "Current directory is $PWD" +``` + +## Multiline + +```sh +cat < { + const codeAttrs = (language !== "") ? ` class="language-${language}"` : ''; + + let link = ""; + switch (language) { + case 'bash': + case 'sh': + case 'zsh': + link = `▶️`; + break; + default: + break; + } + + return `
${syntaxHighlight(code, language)}${link}
`; +}; + +const syntaxHighlight = (code: string, language: string): string => { + if (language && hljs.getLanguage(language)) { + try { + return hljs.highlight(code, { language: language, ignoreIllegals: true }).value; + } catch (err) { + console.error(err); + } + } + return engine.utils.escapeHtml(code); +}; + +export const engine = require('markdown-it')({ + html: true, + linkify: true, + typographer: true, + highlight: renderCodeBlock +}); diff --git a/src/slugify.ts b/src/slugify.ts new file mode 100644 index 0000000..bd1f5b0 --- /dev/null +++ b/src/slugify.ts @@ -0,0 +1,200 @@ +// Borrowed from https://github.com/yzhang-gh/vscode-markdown +import { engine } from './engine'; + +export const enum SlugifyMode { + /** Azure DevOps */ + azure = "azureDevops", + + /** Bitbucket Cloud */ + bitbucket = "bitbucket-cloud", + + /** gitea */ + gitea = "gitea", + + /** github */ + github = "github", + + /** gitlab */ + gitlab = "gitlab", + + /** Visual Studio Code */ + vscode = "vscode", +} + +const utf8Encoder = new TextEncoder(); + +// Converted from Ruby regular expression `/[^\p{Word}\- ]/u` +// `\p{Word}` => Letter (Ll/Lm/Lo/Lt/Lu), Mark (Mc/Me/Mn), Number (Nd/Nl), Connector_Punctuation (Pc) +// It's weird that Ruby's `\p{Word}` actually does not include Category No. +// https://ruby-doc.org/core/Regexp.html +// https://rubular.com/r/ThqXAm370XRMz6 +/** + * The definition of punctuation from github and gitlab. + */ +const regexGithubPunctuation = /[^\p{L}\p{M}\p{Nd}\p{Nl}\p{Pc}\- ]/gu; + +const regexGitlabProductSuffix = /[ \t\r\n\f\v]*\**\((?:core|starter|premium|ultimate)(?:[ \t\r\n\f\v]+only)?\)\**/g; + +/** + * Converts a string of CommonMark **inline** structures to plain text + * by removing Markdown syntax in it. + * This function is only for the `github` and `gitlab` slugify functions. + * @see + * + * @param text - The Markdown string. + * @param env - The markdown-it environment sandbox (**mutable**). + * If you don't provide one properly, we cannot process reference links, etc. + */ +const mdInlineToPlainText = (text: string, env: object): string => { + // Use a clean CommonMark only engine to avoid interfering with plugins from other extensions. + // Use `parseInline` to avoid parsing the string as blocks accidentally. + // See #567, #585, #732, #792; #515; #179; #175, #575 + const inlineTokens = engine.parseInline(text, env)[0].children!; + + const reduceFunc = (result: any, token: { type: any; content: any; }) => { + switch (token.type) { + case "image": + case "html_inline": + return result; + default: + return result + token.content; + } + }; + + return inlineTokens.reduce(reduceFunc, ""); +}; + +/** + * Slugify methods. + * + * Each key is a slugify mode. + * A values is the corresponding slugify function, whose signature must be `(rawContent: string, env: object) => string`. + */ +const slugifyModes: { readonly [mode in SlugifyMode]: (rawContent: string, env: object) => string; } = { + // Sort in alphabetical order. + + [SlugifyMode.azure]: (slug: string): string => { + // https://markdown-all-in-one.github.io/docs/specs/slugify/azure-devops.html + // Encode every character. Although opposed by RFC 3986, it's the only way to solve #802. + return Array.from( + utf8Encoder.encode( + slug + .trim() + .toLowerCase() + .replace(/\p{Zs}/gu, "-") + ), + (b) => "%" + b.toString(16) + ) + .join("") + .toUpperCase(); + }, + + [SlugifyMode.bitbucket]: (slug: string, env: object): string => { + // https://support.atlassian.com/bitbucket-cloud/docs/readme-content/ + // https://bitbucket.org/tutorials/markdowndemo/ + slug = "markdown-header-" + + slugifyModes.github(slug, env).replace(/-+/g, "-"); + + return slug; + }, + + [SlugifyMode.gitea]: (slug: string): string => { + // gitea uses the blackfriday parser + // https://godoc.org/github.com/russross/blackfriday#hdr-Sanitized_Anchor_Names + slug = slug + .replace(/^[^\p{L}\p{N}]+/u, "") + .replace(/[^\p{L}\p{N}]+$/u, "") + .replace(/[^\p{L}\p{N}]+/gu, "-") + .toLowerCase(); + + return slug; + }, + + [SlugifyMode.github]: (slug: string, env: object): string => { + // According to an inspection in 2020-12, github passes the raw content as is, + // and does not trim leading or trailing C0, Zs characters in any step. + // + slug = mdInlineToPlainText(slug, env) + .replace(regexGithubPunctuation, "") + .toLowerCase() // According to an inspection in 2020-09, github performs full Unicode case conversion now. + .replace(/ /g, "-"); + + return slug; + }, + + [SlugifyMode.gitlab]: (slug: string, env: object): string => { + // https://gitlab.com/help/user/markdown + // https://docs.gitlab.com/ee/api/markdown.html + // https://docs.gitlab.com/ee/development/wikis.html + // + // https://gitlab.com/gitlab-org/gitlab/blob/a8c5858ce940decf1d263b59b39df58f89910faf/lib/gitlab/utils/markdown.rb + slug = mdInlineToPlainText(slug, env) + .replace(/^[ \t\r\n\f\v]+/, "") + .replace(/[ \t\r\n\f\v]+$/, "") // https://ruby-doc.org/core/String.html#method-i-strip + .toLowerCase() + .replace(regexGitlabProductSuffix, "") + .replace(regexGithubPunctuation, "") + .replace(/ /g, "-") // Replace space with dash. + .replace(/-+/g, "-") // Replace multiple/consecutive dashes with only one. + + // digits-only hrefs conflict with issue refs + .replace(/^(\d+)$/, "anchor-$1"); + + return slug; + }, + + [SlugifyMode.vscode]: (rawContent: string, env: object): string => { + // https://github.com/microsoft/vscode/blob/0798d13f10b193df0297e301affe761b90a8bfa9/extensions/markdown-language-features/src/slugify.ts#L22-L29 + return encodeURI( + // Simulate . + // Not the same, but should cover most needs. + engine.parseInline(rawContent, env)[0].children! + .reduce((result: any, token: { content: any }) => result + token.content, "") + .trim() + .toLowerCase() + .replace(/\s+/g, "-") // Replace whitespace with - + .replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, "") // Remove known punctuators + .replace(/^\-+/, "") // Remove leading - + .replace(/\-+$/, "") // Remove trailing - + ); + } +}; + +/** + * Slugify a string. + * @param heading - The raw content of the heading according to the CommonMark Spec. + * @param env - The markdown-it environment sandbox (**mutable**). + * @param mode - The slugify mode. + */ +export const slugify = (heading: string, { + env = Object.create(null), + mode = SlugifyMode.github, +}: { env?: object; mode?: SlugifyMode; }) => { + + // Do never twist the input here! + // Pass the raw heading content as is to slugify function. + + // Sort by popularity. + switch (mode) { + case SlugifyMode.github: + return slugifyModes[SlugifyMode.github](heading, env); + + case SlugifyMode.gitlab: + return slugifyModes[SlugifyMode.gitlab](heading, env); + + case SlugifyMode.gitea: + return slugifyModes[SlugifyMode.gitea](heading, env); + + case SlugifyMode.vscode: + return slugifyModes[SlugifyMode.vscode](heading, env); + + case SlugifyMode.azure: + return slugifyModes[SlugifyMode.azure](heading, env); + + case SlugifyMode.bitbucket: + return slugifyModes[SlugifyMode.bitbucket](heading, env); + + default: + return slugifyModes[SlugifyMode.github](heading, env); + } +}; diff --git a/src/tothom.ts b/src/tothom.ts index 432bfea..580866f 100644 --- a/src/tothom.ts +++ b/src/tothom.ts @@ -1,15 +1,19 @@ import * as vscode from 'vscode'; -import hljs from 'highlight.js'; import * as utils from './utils'; import * as terminal from './terminal'; +import { engine } from './engine'; +import { slugify } from './slugify'; const CONFIGURATION_ROOT = 'tothom'; const WEBVIEW_PANEL_TYPE = 'tothom'; +const originalHeadingOpen = engine.renderer.rules.heading_open; + export class Tothom { private _config: vscode.WorkspaceConfiguration; private _views: Map; + private _slugCount = new Map(); constructor(private readonly _extensionUri: vscode.Uri) { this._config = vscode.workspace.getConfiguration(CONFIGURATION_ROOT); @@ -57,8 +61,11 @@ export class Tothom { return undefined; } + engine.renderer.rules.heading_open = this.headingOpen; + this._slugCount.clear(); + const content = utils.readFileContent(resource); - const htmlContent = this.renderHtmlContent(webview, resource, markdownIt.render(content)); + const htmlContent = this.renderHtmlContent(webview, resource, engine.render(content)); webview.html = htmlContent; return webview; @@ -135,39 +142,26 @@ export class Tothom { term.show(); }; -} -const renderCodeBlock = (code: string, language: string): string => { - const codeAttrs = (language !== "") ? ` class="language-${language}"` : ''; - - let link = ""; - switch (language) { - case 'bash': - case 'sh': - case 'zsh': - link = `▶️`; - break; - default: - break; - } + private headingOpen = (tokens: any[], idx: number, options: Object, env: Object, self: any) => { + const raw = tokens[idx + 1].content; + let slug = slugify(raw, { env }); + + let lastCount = this._slugCount.get(slug); + if (lastCount) { + lastCount++; + this._slugCount.set(slug, lastCount); + slug += '-' + lastCount; + } else { + this._slugCount.set(slug, 0); + } - return `
${syntaxHighlight(code, language)}${link}
`; -}; + tokens[idx].attrs = [...(tokens[idx].attrs || []), ["id", slug]]; -const syntaxHighlight = (code: string, language: string): string => { - if (language && hljs.getLanguage(language)) { - try { - return hljs.highlight(code, { language: language, ignoreIllegals: true }).value; - } catch (err) { - console.error(err); + if (originalHeadingOpen) { + return originalHeadingOpen(tokens, idx, options, env, self); + } else { + return self.renderToken(tokens, idx, options); } - } - return markdownIt.utils.escapeHtml(code); -}; - -const markdownIt = require('markdown-it')({ - html: true, - linkify: true, - typographer: true, - highlight: renderCodeBlock -}); + }; +} From e03d76b193ea332b86e0ad24cb4e15f35f338dad Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Tue, 3 Jan 2023 20:42:53 +0100 Subject: [PATCH 3/4] add support for task/todo lists "- [ ]" list items converted to check boxes --- media/github-markdown.css | 9 +++++++++ package-lock.json | 15 +++++++++++++-- package.json | 7 ++++--- src/engine.ts | 4 +++- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/media/github-markdown.css b/media/github-markdown.css index 31a7fae..54ad5af 100644 --- a/media/github-markdown.css +++ b/media/github-markdown.css @@ -855,6 +855,15 @@ body, .tothom-body ol ul { margin-top: 0; margin-bottom: 0; + padding-left: 2em; +} + +.tothom-body .contains-task-list { + padding: 0 0.7em; +} + +.tothom-body li.task-list-item { + list-style-type: none; } .tothom-body li>p { diff --git a/package-lock.json b/package-lock.json index b9c0932..a9872ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "tothom", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "tothom", - "version": "0.0.1", + "version": "0.1.0", "dependencies": { "highlight.js": "^11.7.0", "markdown-it": "^13.0.1", + "markdown-it-task-lists": "^2.1.1", "url-parse": "^1.5.10" }, "devDependencies": { @@ -1656,6 +1657,11 @@ "markdown-it": "bin/markdown-it.js" } }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==" + }, "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", @@ -3767,6 +3773,11 @@ "uc.micro": "^1.0.5" } }, + "markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==" + }, "mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", diff --git a/package.json b/package.json index 2d05c66..36ae319 100644 --- a/package.json +++ b/package.json @@ -87,21 +87,22 @@ "test": "node ./out/test/runTest.js" }, "devDependencies": { - "@types/vscode": "^1.74.0", "@types/glob": "^8.0.0", "@types/mocha": "^10.0.1", "@types/node": "16.x", + "@types/vscode": "^1.74.0", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", + "@vscode/test-electron": "^2.2.0", "eslint": "^8.28.0", "glob": "^8.0.3", "mocha": "^10.1.0", - "typescript": "^4.9.3", - "@vscode/test-electron": "^2.2.0" + "typescript": "^4.9.3" }, "dependencies": { "highlight.js": "^11.7.0", "markdown-it": "^13.0.1", + "markdown-it-task-lists": "^2.1.1", "url-parse": "^1.5.10" } } diff --git a/src/engine.ts b/src/engine.ts index 4e45055..34189d9 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -30,9 +30,11 @@ const syntaxHighlight = (code: string, language: string): string => { return engine.utils.escapeHtml(code); }; +const taskLists = require('markdown-it-task-lists'); + export const engine = require('markdown-it')({ html: true, linkify: true, typographer: true, highlight: renderCodeBlock -}); +}).use(taskLists); From a9c8e3f662f5077a761443c8eace08975d088418 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Tue, 3 Jan 2023 21:09:24 +0100 Subject: [PATCH 4/4] changelog --- CHANGELOG.md | 4 ++++ README.md | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6526371..b81956e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ This file is structured according to the [Keep a Changelog](http://keepachangelo ## [Unreleased] +- Find widget enabled in the preview window +- Support for anchor links +- Support for task/todo lists + ## [v0.1.0] - Initial release diff --git a/README.md b/README.md index fc80e80..44a8832 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,12 @@ Tothom will not render the ▶️ _Run in terminal_ button for code b ## Release Notes +### Unreleased + +- Find widget enabled in the preview window +- Support for anchor links +- Support for task/todo lists + ### v0.1.0 Initial release.