From 78a1af682adbaebff9ce51e81ded07f03388ebdd Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 25 Jan 2024 18:07:07 -0800 Subject: [PATCH 01/17] Fix setting grammar in `markdown-preview` --- packages/markdown-preview/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/markdown-preview/package.json b/packages/markdown-preview/package.json index 67134120c8..724503bba3 100644 --- a/packages/markdown-preview/package.json +++ b/packages/markdown-preview/package.json @@ -91,7 +91,7 @@ "default": "" }, "useOriginalParser": { - "description": "Wether to use the original Markdown Parser, or the new Pulsar one.", + "description": "Whether to use the original Markdown Parser, or the new Pulsar internal one.", "type": "boolean", "default": "true" } From f52e833c22ab32fbf40ec3be7383b79f7175b862 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 26 Jan 2024 00:25:23 -0800 Subject: [PATCH 02/17] Complete most major refactor on `atom.ui.markdown` --- src/ui.js | 593 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 326 insertions(+), 267 deletions(-) diff --git a/src/ui.js b/src/ui.js index 2a4662c443..f496ca450c 100644 --- a/src/ui.js +++ b/src/ui.js @@ -30,81 +30,280 @@ const mdComponents = { }; /** - * @function renderMarkdown + * @function render * @memberof markdown - * @alias render - * @desc Takes a Markdown document and renders it as HTML. - * @param {string} content - The Markdown source material. - * @param {object} givenOpts - The optional arguments: - * @param {string} givenOpts.renderMode - Determines how the page is rendered. - * Valid values "full" or "fragment". + * @desc Processes the actual rendering of markdown content. + * @param {string} content - The string of Markdown. + * @param {object} givenOpts - The optional arguments + * @param {boolean} givenOpts.frontMatter - Whether frontmatter data should be + * processed and displayed. + * @param {boolean} givenOpts.sanitize - Whether sanitization should be applied. + * @param {boolean} givenOpts.sanitizeAllowUnknownProtocols - Whether DOMPurify's + * `ALLOW_UNKNOWN_PROTOCOLS` should be enabled. + * @param {boolean} givenOpts.sanitizeAllowSelfClose - Whether DOMPurify's + * `ALLOW_SELF_CLOSE_IN_ATTR` should be enabled. + * @param {string} givenOpts.renderMode - Determines how the page is returned. + * `full` or `fragment` applies only when Syntax Highlighting. + * @param {string|object} givenOpts.defaultGrammar - An instance of a Pulsar Grammar + * or string, which will be used as the default grammar to apply to code blocks. + * @param {boolean|function} givenOpts.highlight - Determines if Syntax Highlighting + * is applied. Can be a boolean, with true applying syntax highlighting. Or it can + * be a function, which will be used to resolve fenced code block scope names to + * a Pulsar language grammar. + * @param {object} mdInstance - An optional instance of MarkdownIT. Retreived from + * `atom.ui.markdown.buildRenderer()`. + */ +function render(content, givenOpts = {}, mdInstance) { + // Define our default opts to create a full options object + const defaultOpts = { + frontMatter: true, // Determines if Front Matter content should be parsed + sanitize: true, // Enable or disable sanitization of Markdown output + sanitizeAllowUnknownProtocols: true, // pass the value of `ALLOW_UNKNOWN_PROTOCOLS` to DomPurify + sanitizeAllowSelfClose: true, // pass the value of `ALLOW_SELF_CLOSE_IN_ATTR` to DomPurify + highlight: false, // This enables syntax highlighting. Can be true or a function + // to resolve scope names + defaultGrammar: null, // Allows passing a Pulsar Grammar to default to that + // language if applicable, or otherwise allows passing a new default language, + // if excluded, default becomes 'text'. This is an unresolved scope fence + renderMode: "full", // Determines what type of content is returned during + // syntax highlighting, can be `full` or `fragment`. `fragment` is recommended + // for most applications. + }; + + let opts = { ...defaultOpts, ...givenOpts }; + + // Some options have changed since the initial implmentation of the `atom.ui.markdown` + // feature. We will pass along the values of no longer used config options, to + // ensure backwards compatibility. + opts.frontMatter = givenOpts.handleFrontMatter ?? defaultOpts.frontMatter; + opts.highlight = givenOpts.syntaxScopeNameFunc ?? defaultOpts.highlight; + opts.defaultGrammar = givenOpts.grammar ?? defaultOpts.defaultGrammar; + // End of backwards compaitbility options + // Maybe we should emit a warning or deprecation when one is used? + + let md; + + if (mdInstance) { + // We have been provided a markdown instance from `buildRenderer()` so we + // can use that + md = mdInstance; + } else { + // No instance was provided, lets make our own + // We will pass all values that we were given onto the `buildRenderer` func + md = buildRenderer(givenOpts); + } + + let textContent; + + if (opts.frontMatter) { + mdComponents.deps.yamlFrontMatter ??= require("yaml-front-matter"); + const { __content, vars } = mdComponents.deps.yamlFrontMatter.loadFront(content); + + const renderYamlTable = (variables) => { + if (typeof variables === "undefined") { + return ""; + } + + const entries = Object.entries(variables); + + if (!entries.length) { + return ""; + } + + const markdownRows = [ + entries.map(entry => entry[0]), + entries.map(entry => '--'), + entries.map((entry) => { + if (typeof entry[1] === "object" && !Array.isArray(entry[1])) { + // Remove all newlines, or they ruin formatting of parent table + return md.render(renderYamlTable(entry[1])).replace(/\n/g, ""); + } else { + return entry[1]; + } + }) + ]; + + return ( + markdownRows.map(row => "| " + row.join(" | ") + " |").join("\n") + "\n" + ); + }; + + textContent = renderYamlTable(vars) + __content; + } else { + textContent = content; + } + + // Now time to render the content + let rendered = md.render(textContent); + + if (opts.sanitize) { + mdComponents.deps.domPurify ??= require("dompurify"); + + let domPurifyOpts = { + ALLOW_UNKNOWN_PROTOCOLS: opts.sanitizeAllowUnknownProtocols, + ALLOW_SELF_CLOSE_IN_ATTR: opts.sanitizeAllowSelfClose + }; + + rendered = mdComponents.deps.domPurify.sanitize(rendered, opts); + } + + // We now could return this text as ready to go, but lets check if we can + // apply any syntax highlighting + if (opts.highlight) { + // Checking above for truthy should match for if it's a function or true boolean + const convertToDOM = (data) => { + const template = document.createElement("template"); + template.innerHTML = data; + const fragment = template.content.cloneNode(true); + return fragment; + }; + + const domHTMLFragment = convertToDOM(rendered); + + // Now it's time to apply the actual syntax highlighting to our html fragment + const scopeForFenceName = (fence) => { + if (typeof opts.highlight === "function") { + return opts.highlight(fence); + } else { + // TODO mimick the system we built into `markdown-preview` for this + // We could build one in, or just return default + return "text.plain"; + } + }; + + let defaultLanguage; + const fontFamily = atom.config.get("editor.fontFamily"); + + if (opts.defaultGrammar?.scopeName === "source.litcoffee") { + // This is so that we can support defaulting to coffeescript if writing in + // 'source.litcoffee' and rendering our markdown + defaultLanguage = "coffee"; + } else if (typeof opts.defaultGrammar === "string") { + defaultLanguage = opts.defaultGrammar; + } else { + defaultLanguage = "text"; + } + + if (fontFamily) { + for (const codeElement of content.querySelectorAll("code")) { + codeElement.style.fontFamily = fontFamily; + } + } + + let editorCallback; + + if (opts.renderMode === "fragment") { + editorCallback = makeAtomEditorNonInteractive; + } else { + editorCallback = convertAtomEditorToStandardElement; + } + + const promises = []; + for (const preElement of domHTMLFragment.querySelectorAll("pre")) { + const codeBlock = preElement.firstElementChild ?? preElement; + const className = codeBlock.getAttribute("class"); + const fenceName = + className != null ? className.replace(/^language-/, "") : defaultLanguage; + + const editor = new TextEditor({ + readonly: true, + keyboardInputEnabled: false + }); + const editorElement = editor.getElement(); + + preElement.classList.add("editor-colors", `lang-${fenceName}`); + editorElement.setUpdatedSynchronously(true); + preElement.innerHTML = ""; + preElement.parentNode.insertBefore(editorElement, preElement); + editor.setText(codeBlock.textContent.replace(/\r?\n$/, "")); + atom.grammars.assignLanguageMode(editor, scopeForFenceName(fenceName)); + editor.setVisible(true); + + promises.push(editorCallback(editorElement, preElement)); + } + + // Since we don't want to force this function to always be async, as it's only + // needed to be async for this syntax highlighting call, we will instead return + // an async function that can awaited on + return async () => { + await Promise.all(promises); + return domHTMLFragment; + }; + // await Promise.all(promises); + // + // return domHTMLFragment; + } + + // We aren't preforming any syntax highlighting, so lets return our rendered + // text. + return rendered; +} + +/** + * @function buildRenderer + * @memberof markdown + * @desc Returns a Markdown Renderer instance with the provided options. + * Helpful to avoid having to build a new one over and over. + * @param {object} givenOpts - The optional arguments * @param {boolean} givenOpts.html - Whether HTML tags should be allowed. - * @param {boolean} givenOpts.sanitize - If the page content should be saniized via DOMPurify. - * @param {boolean} givenOpts.sanitizeAllowUnknownProtocols - Controls DOMPurify's - * own option of 'ALLOW_UNKNOWN_PROTOCOLS'. - * @param {boolean} givenOpts.sanitizeAllowSelfClose - Controls DOMPurify's - * own option of 'ALLOW_SELF_CLOSE' * @param {boolean} givenOpts.breaks - If newlines should always be converted * into breaklines. - * @param {boolean} givenOpts.handleFrontMatter - Whether frontmatter data should - * processed and displayed. - * @param {boolean} givenOpts.useDefaultEmoji - Whether `markdown-it-emoji` should be enabled. - * @param {boolean} givenOpts.useGitHubHeadings - Whether `markdown-it-github-headings` - * should be enabled. False by default. - * @param {boolean} givenOpts.useTaskCheckbox - Whether `markdown-it-task-checkbox` - * should be enabled. True by default. + * @param {boolean} givenOpts.emoji - If emojis should be included. + * @param {boolean} givenOpts.githubHeadings - Whether `markdown-it-github-headings` + * should be enabled. + * @param {boolean} givenOpts.taskCheckbox - Whether `markdown-it-task-checkbox` + * should be enabled. * @param {boolean} givenOpts.taskCheckboxDisabled - Controls `markdown-it-task-checkbox` - * `disabled` option. True by default. - * @param {boolean} givenOpts.taskCheckboxDivWrap - Controls `markdown-it-task-checkboc` - * `divWrap` option. False by default. - * @param {boolean} givenOpts.transformImageLinks - Attempt to resolve image URLs. - * True by default. - * @param {boolean} givenOpts.transformAtomLinks - Attempt to resolve links - * pointing to Atom. True by Default. - * @param {boolean} givenOpts.transformNonFqdnLinks - Attempt to resolve links - * that are not fully qualified domain names. True by Default. + * `disabled` option. + * @param {boolean} givenOpts.taskCheckboxDivWrap - Controls `markdown-it-task-checkbox` + * `divWrap` option. + * @param {boolean} givenOpts.transformImageLinks - If links to images should + * attempted to be resolved. + * @param {boolean} givenOpts.transformNonFqdnLinks - If non-FQDN links should be + * resolved. + * @param {boolean} givenOpts.transformAtomLinks - If links to Atom pages should + * resolved to the Pulsar equivolant. * @param {string} givenOpts.rootDomain - The root URL of the online resource. - * Useful when attempting to resolve any links on the page. Only works for online - * resources. - * @param {string} givenOpts.filePath - The local alternative to `rootDomain`. - * Used to resolve incomplete paths, but locally on the file system. - * @param {string} givenOpts.disabledMode - The level of disabling of markdown features. - * `none` by default. But supports: "none", "strict" - * @returns {string} Parsed HTML content. + * Used when resolving links. + * @param {string} givenOpts.filePath - The path to the local resource. + * Used when resolving links. + * @param {string} givenOpts.disableMode - The level of disabling to apply. + * @return {object} An instance of a MarkdownIT. */ -function renderMarkdown(content, givenOpts = {}) { - // First we will setup our markdown renderer instance according to the opts provided +function buildRenderer(givenOpts = {}) { + // Define our default opts to create a full options object const defaultOpts = { - renderMode: "full", // Determines if we are rendering a fragment or full page. - // Valid values: 'full', 'fragment' html: true, // Enable HTML tags in source - sanitize: true, // Enable or disable sanitization - sanitizeAllowUnknownProtocols: true, - sanitizeAllowSelfClose: true, - breaks: false, // Convert `\n` in paragraphs into `
` - handleFrontMatter: true, // Determines if Front Matter content should be parsed - useDefaultEmoji: false, // Use `markdown-it-emoji` - useGitHubHeadings: false, // Use `markdown-it-github-headings` - useTaskCheckbox: true, // Use `markdown-it-task-checkbox` - taskCheckboxDisabled: true, // `markdown-it-task-checkbox`: Disable checkbox interactivity - taskCheckboxDivWrap: false, // `markdown-it-task-checkbox`: Wrap div arround checkboc + breaks: true, // Convert `\n` in paragraphs into `
` + emoji: true, // enable or disable emojis + githubHeadings: true, // Use `markdown-it-github-headings` + taskCheckbox: true, // Use `markdown-it-task-checkbox` + taskCheckboxDisabled: true, // For `taskCheckbox`: Disable checkbox interactivity + taskCheckboxDivWrap: true, // For `taskCheckbox`: Wrap div arround checkbox transformImageLinks: true, // Attempt to resolve image urls - transformAtomLinks: true, // Attempt to rewrite links to Atom pages, changing them to Pulsar + rootDomain: "", // the root URL that should be used for attempted translations + filePath: "", // the local path to use during translations transformNonFqdnLinks: true, // Attempt to resolve non-FQDN links - rootDomain: "", // The root URL that should be used for the above 'transform' options - filePath: "", // The path to the file where this markdown is generated from, - disableMode: "none", // The level of disabling that should be done on the output. - // Provides helpful defaults to control how much or how little is disabled: - // - none: Nothing is disabled - // - strict: Everything possible is disabled, except what is otherwise needed + transformAtomLinks: true, // Attempt to rewrite links to Atom pages to the Pulsar equivolant + disableMode: "none", // The level of disabling that should be set. + // - none: Nothing is disabled, the default + // - strict: Most everything is disabled. }; let opts = { ...defaultOpts, ...givenOpts }; - const validateRootDomain = () => { - return typeof opts.rootDomain === "string" && opts.rootDomain.length > 1; - }; - + // Some options have changed since the initial implmentation of the `atom.ui.markdown` + // feature. We will pass along the values of no longer used config options, to + // ensure backwards compatibility. + opts.emoji = givenOpts.useDefaultEmoji ?? defaultOpts.emoji; + opts.githubHeadings = givenOpts.useGitHubHeadings ?? defaultOpts.githubHeadings; + opts.taskCheckbox = givenOpts.useTaskCheckbox ?? defaultOpts.taskCheckbox; + // End of backwards compaitbility options + // Maybe we should emit a warning or deprecation when one is used? + + // Setup + const validRootDomain = (typeof opts.rootDomain === "string" && opts.rootDomain.length > 1); + const validLocalItem = (typeof opts.filePath === "string" && opts.filePath.length > 1); const cleanRootDomain = () => { // We will also remove any trailing `/` as link resolvers down the line add them in return opts.rootDomain.replace(".git", "").replace(/\/$/, ""); @@ -115,35 +314,31 @@ function renderMarkdown(content, givenOpts = {}) { breaks: opts.breaks }; - let md = new MarkdownIt(markdownItOpts); + const md = new MarkdownIt(markdownItOpts); - if (opts.useDefaultEmoji) { + // Hook up emojis + if (opts.emoji) { mdComponents.deps.markdownItEmoji ??= require("markdown-it-emoji"); md.use(mdComponents.deps.markdownItEmoji, {}); } - if (opts.useGitHubHeadings) { + if (opts.githubHeadings) { mdComponents.deps.markdownItGitHubHeadings ??= require("markdown-it-github-headings"); md.use(mdComponents.deps.markdownItGitHubHeadings, {}); } - if (opts.useTaskCheckbox) { + if (opts.taskCheckbox) { mdComponents.deps.markdownItTaskCheckbox ??= require("markdown-it-task-checkbox"); md.use(mdComponents.deps.markdownItTaskCheckbox, { disabled: opts.taskCheckboxDisabled, divWrap: opts.taskCheckboxDivWrap }); } - if (opts.transformImageLinks && validateRootDomain()) { + + // Hook up custom rules + if (opts.transformImageLinks) { // Here we will take any links for images provided in the content, and do // our best to ensure they can accurately resolve. - const defaultImageRenderer = md.renderer.rules.image; // We want to keep access to this - - // Determines when we handle links if the item could be a local file or not - let couldBeLocalItem; - if (typeof opts.filePath != "string" || opts.filePath.length < 1) { - couldBeLocalItem = false; - } else { - couldBeLocalItem = true; - } + const defaultImageRenderer = md.renderer.rules.image; // We want to keep a + // reference to this md.renderer.rules.image = (tokens, idx, options, env, self) => { let token = tokens[idx]; @@ -152,42 +347,61 @@ function renderMarkdown(content, givenOpts = {}) { // Lets say content contains './my-cool-image.png' // We need to turn it into something like this: // https://github.com/USER/REPO/raw/HEAD/my-cool-image.png - if (mdComponents.reg.localLinks.currentDir.test(token.attrGet("src"))) { + if (mdComponents.reg.localLinks.curentDir.test(token.attrGet("src"))) { let rawLink = token.attrGet("src"); rawLink = rawLink.replace(mdComponents.reg.localLinks.currentDir, ""); - // Now we need to handle links for both the web and locally - // We can do this by first checking if the link resolves locally - if (couldBeLocalItem) { + // Now that we have the raw link of a local link, we need to handle this + // depending on if the link is local or not + let hasSet = false; + if (validLocalItem) { + let originalSrc = path.resolve(rawLink); let newSrc = path.resolve(path.dirname(opts.filePath, rawLink)); - if (!fs.lstatSync(newSrc).isFile()) { + if (fs.lstatSync(originalSrc).isFile()) { + // the normal link is already a valid local link to the filesystem + token.attrSet("src", originalSrc); + hasSet = true; + } else if (fs.lstatSync(newSrc).isFile()) { + // This link does successfully point to the filesystem after being + // merged with the provided filePath token.attrSet("src", newSrc); - } else { - token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); + hasSet = true; } - } else { + } + if (validRootDomain && !hasSet) { + // Attempt to resolve remotely only if this is a valid root domain provided + // and we didn't already set it when checking locally + + // TODO this should not assume that the only image host is GitHub token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); } } else if (mdComponents.reg.localLinks.rootDir.test(token.attrGet("src"))) { + // tests for any links pointing to the root dir `/` let rawLink = token.attrGet("src"); rawLink = rawLink.replace(mdComponents.reg.localLinks.rootDir, ""); - // Now to handle the possible web or local link - if (couldBeLocalItem) { + // Now to handle the possible we or local link resolving + let hasSet = false; + if (validLocalItem) { const [rootDirectory] = atom.project.relativePath(opts.filePath); - if (!fs.lstatSync(src).isFile() && rootDirectory) { + if (fs.lstatSync(src).isFile() && rootDirectory) { let newSrc = path.join(rootDirectory, rawLink); token.attrSet("src", newSrc); - } else { - token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); + hasSet = true; } - } else { + } + if (validRootDomain && !hasSet) { + // TODO again we shouldn't assume this image is on GitHub token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); + hasSet = true; } } else if (!token.attrGet("src").startsWith("http") && !mdComponents.reg.globalLinks.base64.test(token.attrGet("src"))) { - // Check for implicit relative urls + // This looks like an implicit relative url let rawLink = token.attrGet("src"); + // TODO Don't assume GitHub. At this point it's obvious where these checks + // originated from, and that they need to be expanded token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); - } else if ([".gif", ".png", ".jpg", ".jpeg", ".webp"].find(ext => token.attrGet("src").endsWith(ext)) && token.attrGet("src").startsWith("https://github.com") && token.attrGet("src").includes("blob")) { - // Should match any image being distributed from GitHub that's using `blob` instead of `raw` causing images to not load correctly + } else if ([ ".git", ".png", ".jpg", ".jpeg", ".webp"].find(ext => token.attrGet("src").endsWith(ext)) && token.attrGet("src").startsWith("https://github.com") && token.attrGet("src").includes("blob")) { + // Should match images being distributed from GitHub that's using `blob` instead of `raw` + // which will cause images to fail to load. let rawLink = token.attrGet("src"); token.attrSet("src", rawLink.replace("blob", "raw")); } @@ -196,7 +410,8 @@ function renderMarkdown(content, givenOpts = {}) { return defaultImageRenderer(tokens, idx, options, env, self); }; } - if (validateRootDomain() && opts.transformNonFqdnLinks) { + + if (validRootDomain && opts.transformNonFqdnLinks) { md.core.ruler.after("inline", "fix-links", (state) => { state.tokens.forEach((blockToken) => { if (blockToken.type === "inline" && blockToken.children) { @@ -220,8 +435,9 @@ function renderMarkdown(content, givenOpts = {}) { } }); }); - } else if (opts.transformAtomLinks) { - // This is a separate if since transforming Atom links does not need a valid root domain provided + } + + if (opts.transformAtomLinks) { md.core.ruler.after("inline", "fix-atom-links", (state) => { state.tokens.forEach((blockToken) => { if (blockToken.type === "inline" && blockToken.children) { @@ -247,8 +463,8 @@ function renderMarkdown(content, givenOpts = {}) { }); } - // Here we can add some simple additions that make code highlighting possible later on, - // but doesn't actually preform any code highlighting. + // Here we add a simple addition that makes code highlighting possible later on + // but itself doesn't do much to highlight md.options.highlight = function(str, lang) { return `
${str}
`; }; @@ -256,7 +472,6 @@ function renderMarkdown(content, givenOpts = {}) { // Process disables if (opts.disableMode === "strict") { - // Easy Disable md.disable("lheading"); // Disable Code Blocks @@ -286,7 +501,7 @@ function renderMarkdown(content, givenOpts = {}) { // Determine how to best handle this to only allow line breaks. Research needed if (state.src.charAt(state.pos) === "<") { // We only want to act once on the beginning of the inline element - // Then confirm if it's the item we expect + // then confirm if it's the item we expect const textAfterPending = state.src.replace(state.pending, ""); const match = textAfterPending.match(/^/); if (match) { @@ -342,7 +557,7 @@ function renderMarkdown(content, givenOpts = {}) { tokens[idx].type = "text"; tokens[idx].content = ""; - if (tokens[idx].type == endType) { + if (tokens[idx].type === endType) { break; } @@ -364,7 +579,7 @@ function renderMarkdown(content, givenOpts = {}) { }; // Disable Ordered lists - md.renderer.rules.ordered_list_open = (tokens, idx, _options, _env, _self) => { + md.renderer.rules.ordered_list_open = (tokens, idx, _options, _env, _self) => { stripAllTokensTill(tokens, idx, "ordered_list_close"); return ""; }; @@ -373,13 +588,11 @@ function renderMarkdown(content, givenOpts = {}) { md.renderer.rules.html_inline = (tokens, idx, _options, _env, _self) => { if (tokens[idx].type === "html_inline") { // Here we can build an allow list of inline HTML elements to keep. - if ( - tokens[idx].tag !== "breakline" - ) { - return ""; - } else { - return tokens[idx].content; - } + if (tokens[idx].tag !== "breakline") { + return ""; + } else { + return tokens[idx].content; + } } }; @@ -392,163 +605,10 @@ function renderMarkdown(content, givenOpts = {}) { } - let textContent; - - if (opts.handleFrontMatter) { - mdComponents.deps.yamlFrontMatter ??= require("yaml-front-matter"); - const { __content, vars } = mdComponents.deps.yamlFrontMatter.loadFront(content); - - const renderYamlTable = (variables) => { - if (typeof variables === "undefined") { - return ""; - } - - const entries = Object.entries(variables); - - if (!entries.length) { - return ""; - } - - const markdownRows = [ - entries.map(entry => entry[0]), - entries.map(entry => '--'), - entries.map((entry) => { - if (typeof entry[1] === "object" && !Array.isArray(entry[1])) { - // Remove all newlines, or they ruin formatting of parent table - return md.render(renderYamlTable(entry[1])).replace(/\n/g, ""); - } else { - return entry[1]; - } - }) - ]; - - return ( - markdownRows.map(row => "| " + row.join(" | ") + " |").join("\n") + "\n" - ); - }; - - textContent = renderYamlTable(vars) + __content; - } else { - textContent = content; - } - - let rendered = md.render(textContent); - - if (opts.sanitize) { - mdComponents.deps.domPurify ??= require("dompurify"); - - let domPurifyOpts = { - ALLOW_UNKNOWN_PROTOCOLS: opts.sanitizeAllowUnknownProtocols, - ALLOW_SELF_CLOSE_IN_ATTR: opts.sanitizeAllowSelfClose - }; - - rendered = mdComponents.deps.domPurify.sanitize(rendered, opts); - } - - return rendered; -} - -/** - * @function applySyntaxHighlighting - * @memberof markdown - * @async - * @desc Uses Pulsar's built-in Syntax Highlighting system to apply the same syntax - * highlighting to code blocks within markdown. Modifies the existing object passed. - * @param {HTMLFragment} content - The HTML Node/Fragment to apply syntax highlighting on. - * Will modifyn the original object. - * @param {object} givenOpts - Optional Arguments: - * @param {function} givenOpts.syntaxScopeNameFunc - A function that can be called with - * any given language ID from a code block scope, and returns the grammar source id - * that should be used to preform syntax highlighting. - * @param {string} givenOpts.renderMode - Whether we are rdnering a document fragment - * or a full document. Valid values: "full", "fragment". - * @param {object} givenOpts.grammar - The grammar of the source file. Carryover from - * original `markdown-preview` functionality. - */ -function applySyntaxHighlighting(content, givenOpts = {}) { - const defaultOpts = { - syntaxScopeNameFunc: null, // Function used to resolve codeblock fences language id - // to a Pulsar Grammar source. Should be a function that takes the declared scope and returns a source, - grammar: null, - renderMode: "full", // Just like in `renderMarkdown` this can be full or fragment - }; - - const opts = { ...defaultOpts, ...givenOpts }; - - const scopeForFenceName = (fence) => { - if (typeof opts.syntaxScopeNameFunc == "function") { - return opts.syntaxScopeNameFunc(fence); - } else { - // We could build one in, or just return default - return "text.plain"; - } - }; - - let defaultLanguage; - const fontFamily = atom.config.get("editor.fontFamily"); - - if ((opts.grammar != null ? opts.grammar.scopeName : undefined) === "source.litcoffee") { - // This behavior is carried over from `markdown-preview` but it's purpose and need - // is not fully understood. - defaultLanguage = "coffee"; - } else { - defaultLanguage = "text"; - } - - if (fontFamily) { - for (const codeElement of content.querySelectorAll("code")) { - codeElement.style.fontFamily = fontFamily; - } - } - - let editorCallback; + // Done processing restrictions - if (opts.renderMode === "fragment") { - editorCallback = makeAtomEditorNonInteractive; - } else { - // Captures full and defaults - editorCallback = convertAtomEditorToStandardElement; - } - - const promises = []; - for (const preElement of content.querySelectorAll("pre")) { - const codeBlock = preElement.firstElementChild ?? preElement; - const className = codeBlock.getAttribute("class"); - const fenceName = - className != null ? className.replace(/^language-/, "") : defaultLanguage; - - const editor = new TextEditor({ - readonly: true, - keyboardInputEnabled: false - }); - const editorElement = editor.getElement(); - - preElement.classList.add("editor-colors", `lang-${fenceName}`); - editorElement.setUpdatedSynchronously(true); - preElement.innerHTML = ""; - preElement.parentNode.insertBefore(editorElement, preElement); - editor.setText(codeBlock.textContent.replace(/\r?\n$/, "")); - atom.grammars.assignLanguageMode(editor, scopeForFenceName(fenceName)); - editor.setVisible(true); - - promises.push(editorCallback(editorElement, preElement)); - } - return Promise.all(promises); -} - -/** - * @function convertToDOM - * @memberof markdown - * @desc Takes a raw HTML string of data and returns a proper HTMLFragment. - * This should be done if you need access to APIs available on the DOM itself. - * @param {string} content - The HTML String. - * @returns {HTMLFragment} - */ -function convertToDOM(content) { - const template = document.createElement("template"); - template.innerHTML = content; - const fragment = template.content.cloneNode(true); - return fragment; + // Return the fully complete markdown instance + return md; } /* @@ -722,12 +782,11 @@ function convertAtomEditorToStandardElement(editorElement, preElement) { * @member markdown * @memberof ui * @desc The Markdown object exported from the UI API. - * Provides access to: ".render", ".applySyntaxHighlighting", ".convertToDOM" + * Provides access to: ".render", ".buildRenderer" */ const markdown = { - render: renderMarkdown, - applySyntaxHighlighting: applySyntaxHighlighting, - convertToDOM: convertToDOM + render: render, + buildRenderer: buildRenderer }; module.exports = { From c1f48bb158d3e078ff5c6de4660aae9542273754 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 26 Jan 2024 00:25:36 -0800 Subject: [PATCH 03/17] Testing implemented changes in `markdown-preview` --- packages/markdown-preview/lib/renderer.js | 75 +++++++++++++++-------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/packages/markdown-preview/lib/renderer.js b/packages/markdown-preview/lib/renderer.js index 636ad68c54..d53819e56c 100644 --- a/packages/markdown-preview/lib/renderer.js +++ b/packages/markdown-preview/lib/renderer.js @@ -30,24 +30,38 @@ exports.toDOMFragment = async function (text, filePath, grammar, callback) { } else { // We use the new parser! - const domFragment = atom.ui.markdown.render(text, + const domFragment = await atom.ui.markdown.render( + text, { renderMode: "fragment", filePath: filePath, - breaks: atom.config.get('markdown-preview.breakOnSingleNewline'), - useDefaultEmoji: true, - sanitizeAllowUnknownProtocols: atom.config.get('markdown-preview.allowUnsafeProtocols') - } - ); - const domHTMLFragment = atom.ui.markdown.convertToDOM(domFragment); - await atom.ui.markdown.applySyntaxHighlighting(domHTMLFragment, - { - renderMode: "fragment", - syntaxScopeNameFunc: scopeForFenceName, - grammar: grammar + breaks: atom.config.get("markdown-preview.breakOnSingleNewline"), + emoji: true, + sanitize: true, + sanitizeAllowUnknownProtocols: atom.config.get("markdown-preview.allowUnsafeProtocols"), + highlight: scopeForFenceName, + defaultGrammar: grammar } ); + // const domFragment = atom.ui.markdown.render(text, + // { + // renderMode: "fragment", + // filePath: filePath, + // breaks: atom.config.get('markdown-preview.breakOnSingleNewline'), + // useDefaultEmoji: true, + // sanitizeAllowUnknownProtocols: atom.config.get('markdown-preview.allowUnsafeProtocols') + // } + // ); + // const domHTMLFragment = atom.ui.markdown.convertToDOM(domFragment); + // await atom.ui.markdown.applySyntaxHighlighting(domHTMLFragment, + // { + // renderMode: "fragment", + // syntaxScopeNameFunc: scopeForFenceName, + // grammar: grammar + // } + // ); + return domHTMLFragment; } } @@ -71,28 +85,41 @@ exports.toHTML = async function (text, filePath, grammar) { return result } else { // We use the new parser! - const domFragment = atom.ui.markdown.render(text, + const domFragment = await atom.ui.markdown.render( + text, { renderMode: "full", filePath: filePath, - breaks: atom.config.get('markdown-preview.breakOnSingleNewline'), - useDefaultEmoji: true, - sanitizeAllowUnknownProtocols: atom.config.get('markdown-preview.allowUnsafeProtocols') + breaks: atom.config.get("markdown-preview.breakOnSingleNewline"), + emoji: true, + sanitize: true, + sanitizeAllowUnknownProtocols: atom.config.get("markdown-preview.allowUnsafeProtocols"), + highlight: scopeForFenceName, + grammar: grammar } ); - const domHTMLFragment = atom.ui.markdown.convertToDOM(domFragment); + // const domFragment = atom.ui.markdown.render(text, + // { + // renderMode: "full", + // filePath: filePath, + // breaks: atom.config.get('markdown-preview.breakOnSingleNewline'), + // useDefaultEmoji: true, + // sanitizeAllowUnknownProtocols: atom.config.get('markdown-preview.allowUnsafeProtocols') + // } + // ); + // const domHTMLFragment = atom.ui.markdown.convertToDOM(domFragment); const div = document.createElement("div"); div.appendChild(domHTMLFragment); document.body.appendChild(div); - await atom.ui.markdown.applySyntaxHighlighting(div, - { - renderMode: "full", - syntaxScopeNameFunc: scopeForFenceName, - grammar: grammar - } - ); + // await atom.ui.markdown.applySyntaxHighlighting(div, + // { + // renderMode: "full", + // syntaxScopeNameFunc: scopeForFenceName, + // grammar: grammar + // } + // ); const result = div.innerHTML; div.remove(); From a49be04a434ebf329ff5596478b8347dd3e9771e Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 26 Jan 2024 18:12:45 -0800 Subject: [PATCH 04/17] Bring defaults back into line with how they previously were --- src/ui.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui.js b/src/ui.js index f496ca450c..c05f4f9e9c 100644 --- a/src/ui.js +++ b/src/ui.js @@ -275,15 +275,15 @@ function buildRenderer(givenOpts = {}) { const defaultOpts = { html: true, // Enable HTML tags in source breaks: true, // Convert `\n` in paragraphs into `
` - emoji: true, // enable or disable emojis - githubHeadings: true, // Use `markdown-it-github-headings` - taskCheckbox: true, // Use `markdown-it-task-checkbox` + emoji: false, // enable or disable emojis + githubHeadings: false, // Use `markdown-it-github-headings` + taskCheckbox: false, // Use `markdown-it-task-checkbox` taskCheckboxDisabled: true, // For `taskCheckbox`: Disable checkbox interactivity - taskCheckboxDivWrap: true, // For `taskCheckbox`: Wrap div arround checkbox - transformImageLinks: true, // Attempt to resolve image urls + taskCheckboxDivWrap: false, // For `taskCheckbox`: Wrap div arround checkbox + transformImageLinks: false, // Attempt to resolve image urls rootDomain: "", // the root URL that should be used for attempted translations filePath: "", // the local path to use during translations - transformNonFqdnLinks: true, // Attempt to resolve non-FQDN links + transformNonFqdnLinks: false, // Attempt to resolve non-FQDN links transformAtomLinks: true, // Attempt to rewrite links to Atom pages to the Pulsar equivolant disableMode: "none", // The level of disabling that should be set. // - none: Nothing is disabled, the default From 0d8fcf7013f012dc1bfd8d507b5d3385e04b1311 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 26 Jan 2024 19:01:53 -0800 Subject: [PATCH 05/17] Update `settings-view` --- packages/settings-view/lib/package-readme-view.js | 4 ++-- packages/settings-view/lib/rich-description.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/settings-view/lib/package-readme-view.js b/packages/settings-view/lib/package-readme-view.js index 1c47bec8ca..6cd98b8e1e 100644 --- a/packages/settings-view/lib/package-readme-view.js +++ b/packages/settings-view/lib/package-readme-view.js @@ -25,8 +25,8 @@ export default class PackageReadmeView { const markdownOpts = { breaks: false, - taskCheckboxDisabled: true, - useGitHubHeadings: true + taskCheckbox: true, + githubHeadings: true }; if (readmeIsLocal) { diff --git a/packages/settings-view/lib/rich-description.js b/packages/settings-view/lib/rich-description.js index a71f43cb63..b8983c72bc 100644 --- a/packages/settings-view/lib/rich-description.js +++ b/packages/settings-view/lib/rich-description.js @@ -9,7 +9,7 @@ module.exports = { return atom.ui.markdown.render( description, { - useTaskCheckbox: false, + taskCheckbox: false, disableMode: "strict", } ).replace(/

(.*)<\/p>/, "$1").trim(); From 0e3b9b2efca3024aeee452d8eeaa23adf6e262d9 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 26 Jan 2024 20:39:37 -0800 Subject: [PATCH 06/17] Fixed bugs in refactor, found and fixed bugs in local image resolution --- src/ui.js | 78 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/src/ui.js b/src/ui.js index c05f4f9e9c..525b9ffad1 100644 --- a/src/ui.js +++ b/src/ui.js @@ -75,9 +75,9 @@ function render(content, givenOpts = {}, mdInstance) { // Some options have changed since the initial implmentation of the `atom.ui.markdown` // feature. We will pass along the values of no longer used config options, to // ensure backwards compatibility. - opts.frontMatter = givenOpts.handleFrontMatter ?? defaultOpts.frontMatter; - opts.highlight = givenOpts.syntaxScopeNameFunc ?? defaultOpts.highlight; - opts.defaultGrammar = givenOpts.grammar ?? defaultOpts.defaultGrammar; + opts.frontMatter = givenOpts.handleFrontMatter ?? givenOpts.frontMatter ?? defaultOpts.frontMatter; + opts.highlight = givenOpts.syntaxScopeNameFunc ?? givenOpts.highlight ?? defaultOpts.highlight; + opts.defaultGrammar = givenOpts.grammar ?? givenOpts.defaultGrammar ?? defaultOpts.defaultGrammar; // End of backwards compaitbility options // Maybe we should emit a warning or deprecation when one is used? @@ -144,7 +144,7 @@ function render(content, givenOpts = {}, mdInstance) { ALLOW_SELF_CLOSE_IN_ATTR: opts.sanitizeAllowSelfClose }; - rendered = mdComponents.deps.domPurify.sanitize(rendered, opts); + rendered = mdComponents.deps.domPurify.sanitize(rendered, domPurifyOpts); } // We now could return this text as ready to go, but lets check if we can @@ -185,7 +185,7 @@ function render(content, givenOpts = {}, mdInstance) { } if (fontFamily) { - for (const codeElement of content.querySelectorAll("code")) { + for (const codeElement of domHTMLFragment.querySelectorAll("code")) { codeElement.style.fontFamily = fontFamily; } } @@ -229,14 +229,11 @@ function render(content, givenOpts = {}, mdInstance) { await Promise.all(promises); return domHTMLFragment; }; - // await Promise.all(promises); - // - // return domHTMLFragment; + } else { + // We aren't preforming any syntax highlighting, so lets return our rendered + // text. + return rendered; } - - // We aren't preforming any syntax highlighting, so lets return our rendered - // text. - return rendered; } /** @@ -295,9 +292,9 @@ function buildRenderer(givenOpts = {}) { // Some options have changed since the initial implmentation of the `atom.ui.markdown` // feature. We will pass along the values of no longer used config options, to // ensure backwards compatibility. - opts.emoji = givenOpts.useDefaultEmoji ?? defaultOpts.emoji; - opts.githubHeadings = givenOpts.useGitHubHeadings ?? defaultOpts.githubHeadings; - opts.taskCheckbox = givenOpts.useTaskCheckbox ?? defaultOpts.taskCheckbox; + opts.emoji = givenOpts.useDefaultEmoji ?? givenOpts.emoji ?? defaultOpts.emoji; + opts.githubHeadings = givenOpts.useGitHubHeadings ?? givenOpts.githubHeadings ?? defaultOpts.githubHeadings; + opts.taskCheckbox = givenOpts.useTaskCheckbox ?? givenOpts.taskCheckbox ?? defaultOpts.taskCheckbox; // End of backwards compaitbility options // Maybe we should emit a warning or deprecation when one is used? @@ -308,6 +305,18 @@ function buildRenderer(givenOpts = {}) { // We will also remove any trailing `/` as link resolvers down the line add them in return opts.rootDomain.replace(".git", "").replace(/\/$/, ""); }; + const convertPathToLocalProtocol = (link) => { + // Not sure how this wasn't caught before, but when we resolve a local link, + // such as `D:\pulsar-edit\pulsar\resources\readme.png` two things happen: + // If `sanitize: true` then DOMPurify removes all "\" then the resulting URL + // is invalid, but even worse if `sanitizeAllowUnknownProtocols: false` + // the URL will be removed entirely. + // So we need to convert from a valid local path like above, to a valid + // file protocol link, such as `file:///D:/pulsar-edit/pulsar/resources/readme.png` + link = link.replace(/\\/g, "/"); + link = `file:///${link}`; + return link; + }; const markdownItOpts = { html: opts.html, @@ -347,7 +356,7 @@ function buildRenderer(givenOpts = {}) { // Lets say content contains './my-cool-image.png' // We need to turn it into something like this: // https://github.com/USER/REPO/raw/HEAD/my-cool-image.png - if (mdComponents.reg.localLinks.curentDir.test(token.attrGet("src"))) { + if (mdComponents.reg.localLinks.currentDir.test(token.attrGet("src"))) { let rawLink = token.attrGet("src"); rawLink = rawLink.replace(mdComponents.reg.localLinks.currentDir, ""); // Now that we have the raw link of a local link, we need to handle this @@ -356,14 +365,14 @@ function buildRenderer(givenOpts = {}) { if (validLocalItem) { let originalSrc = path.resolve(rawLink); let newSrc = path.resolve(path.dirname(opts.filePath, rawLink)); - if (fs.lstatSync(originalSrc).isFile()) { + if (fs.lstatSync(originalSrc, { throwIfNoEntry: false })?.isFile()) { // the normal link is already a valid local link to the filesystem - token.attrSet("src", originalSrc); + token.attrSet("src", convertPathToLocalProtocol(originalSrc)); hasSet = true; - } else if (fs.lstatSync(newSrc).isFile()) { + } else if (fs.lstatSync(newSrc, { throwIfNoEntry: false })?.isFile()) { // This link does successfully point to the filesystem after being // merged with the provided filePath - token.attrSet("src", newSrc); + token.attrSet("src", convertPathToLocalProtocol(newSrc)); hasSet = true; } } @@ -382,9 +391,9 @@ function buildRenderer(givenOpts = {}) { let hasSet = false; if (validLocalItem) { const [rootDirectory] = atom.project.relativePath(opts.filePath); - if (fs.lstatSync(src).isFile() && rootDirectory) { + if (fs.lstatSync(src, { throwIfNoEntry: false })?.isFile() && rootDirectory) { let newSrc = path.join(rootDirectory, rawLink); - token.attrSet("src", newSrc); + token.attrSet("src", convertPathToLocalProtocol(newSrc)); hasSet = true; } } @@ -396,9 +405,28 @@ function buildRenderer(givenOpts = {}) { } else if (!token.attrGet("src").startsWith("http") && !mdComponents.reg.globalLinks.base64.test(token.attrGet("src"))) { // This looks like an implicit relative url let rawLink = token.attrGet("src"); - // TODO Don't assume GitHub. At this point it's obvious where these checks - // originated from, and that they need to be expanded - token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); + + let hasSet = false; + if (validLocalItem) { + let originalSrc = path.resolve(rawLink); + let newSrc = path.resolve(path.dirname(opts.filePath, rawLink)); + + if (fs.lstatSync(originalSrc, { throwIfNoEntry: false })?.isFile()) { + // the normal link is already a valid local link to the filesystem + token.attrSet("src", convertPathToLocalProtocol(originalSrc)); + hasSet = true; + } else if (fs.lstatSync(newSrc, { throwIfNoEntry: false})?.isFile()) { + // This link does successfully point to the filesystem after being + // merged with the provided filePath + token.attrSet("src", convertPathToLocalProtocol(newSrc)); + hasSet = true; + } + } + if (validRootDomain && !hasSet) { + // TODO again we shouldn't assume this image is one GitHub + token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); + hasSet = true; + } } else if ([ ".git", ".png", ".jpg", ".jpeg", ".webp"].find(ext => token.attrGet("src").endsWith(ext)) && token.attrGet("src").startsWith("https://github.com") && token.attrGet("src").includes("blob")) { // Should match images being distributed from GitHub that's using `blob` instead of `raw` // which will cause images to fail to load. From 8fde886f35e8b3b2505af224353ad37a0abe1176 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 26 Jan 2024 20:39:45 -0800 Subject: [PATCH 07/17] Updated `markdown-preview` --- packages/markdown-preview/lib/renderer.js | 46 +++-------------------- 1 file changed, 5 insertions(+), 41 deletions(-) diff --git a/packages/markdown-preview/lib/renderer.js b/packages/markdown-preview/lib/renderer.js index d53819e56c..4566df1213 100644 --- a/packages/markdown-preview/lib/renderer.js +++ b/packages/markdown-preview/lib/renderer.js @@ -42,27 +42,9 @@ exports.toDOMFragment = async function (text, filePath, grammar, callback) { highlight: scopeForFenceName, defaultGrammar: grammar } - ); - - // const domFragment = atom.ui.markdown.render(text, - // { - // renderMode: "fragment", - // filePath: filePath, - // breaks: atom.config.get('markdown-preview.breakOnSingleNewline'), - // useDefaultEmoji: true, - // sanitizeAllowUnknownProtocols: atom.config.get('markdown-preview.allowUnsafeProtocols') - // } - // ); - // const domHTMLFragment = atom.ui.markdown.convertToDOM(domFragment); - // await atom.ui.markdown.applySyntaxHighlighting(domHTMLFragment, - // { - // renderMode: "fragment", - // syntaxScopeNameFunc: scopeForFenceName, - // grammar: grammar - // } - // ); - - return domHTMLFragment; + )(); + + return domFragment; } } @@ -97,30 +79,12 @@ exports.toHTML = async function (text, filePath, grammar) { highlight: scopeForFenceName, grammar: grammar } - ); - // const domFragment = atom.ui.markdown.render(text, - // { - // renderMode: "full", - // filePath: filePath, - // breaks: atom.config.get('markdown-preview.breakOnSingleNewline'), - // useDefaultEmoji: true, - // sanitizeAllowUnknownProtocols: atom.config.get('markdown-preview.allowUnsafeProtocols') - // } - // ); - // const domHTMLFragment = atom.ui.markdown.convertToDOM(domFragment); + )(); const div = document.createElement("div"); - div.appendChild(domHTMLFragment); + div.appendChild(domFragment); document.body.appendChild(div); - // await atom.ui.markdown.applySyntaxHighlighting(div, - // { - // renderMode: "full", - // syntaxScopeNameFunc: scopeForFenceName, - // grammar: grammar - // } - // ); - const result = div.innerHTML; div.remove(); From 9c6d4834f26e2bf2ed7b619e255d1d72b4c9fb25 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 26 Jan 2024 21:11:20 -0800 Subject: [PATCH 08/17] Update arguments for `settings-view` usage --- packages/settings-view/lib/package-readme-view.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/settings-view/lib/package-readme-view.js b/packages/settings-view/lib/package-readme-view.js index 6cd98b8e1e..84653fb1cb 100644 --- a/packages/settings-view/lib/package-readme-view.js +++ b/packages/settings-view/lib/package-readme-view.js @@ -26,7 +26,9 @@ export default class PackageReadmeView { const markdownOpts = { breaks: false, taskCheckbox: true, - githubHeadings: true + githubHeadings: true, + transformImageLinks: true, + transformNonFqdnLinks: true }; if (readmeIsLocal) { From 480456ec774fc69cb8de455112b1eb01fd3d7ecd Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 26 Jan 2024 21:14:27 -0800 Subject: [PATCH 09/17] Fix a default argument incorrectly set --- src/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui.js b/src/ui.js index 525b9ffad1..3684cb99bb 100644 --- a/src/ui.js +++ b/src/ui.js @@ -274,7 +274,7 @@ function buildRenderer(givenOpts = {}) { breaks: true, // Convert `\n` in paragraphs into `
` emoji: false, // enable or disable emojis githubHeadings: false, // Use `markdown-it-github-headings` - taskCheckbox: false, // Use `markdown-it-task-checkbox` + taskCheckbox: true, // Use `markdown-it-task-checkbox` taskCheckboxDisabled: true, // For `taskCheckbox`: Disable checkbox interactivity taskCheckboxDivWrap: false, // For `taskCheckbox`: Wrap div arround checkbox transformImageLinks: false, // Attempt to resolve image urls From e82864a9338f3371f11df26f672685125a526271 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 26 Jan 2024 23:18:44 -0800 Subject: [PATCH 10/17] Add error logging to `settings-view` to find where CI issue is occuring --- packages/settings-view/lib/package-readme-view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/settings-view/lib/package-readme-view.js b/packages/settings-view/lib/package-readme-view.js index 84653fb1cb..b47702dd0b 100644 --- a/packages/settings-view/lib/package-readme-view.js +++ b/packages/settings-view/lib/package-readme-view.js @@ -40,6 +40,7 @@ export default class PackageReadmeView { try { this.packageReadme.innerHTML = atom.ui.markdown.render(readme, markdownOpts); } catch(err) { + console.error(err); this.packageReadme.innerHTML = "

Error parsing README

"; } } From fa28467c45dca884dd1827a59e248870e36ec9d9 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 28 Jan 2024 03:38:45 -0800 Subject: [PATCH 11/17] Revert link resolution This is more complicated than I gave it credit for. Changing anything here seems to cause test failures, and changes to how this is done will likely require it's own unique PR --- src/ui.js | 78 ++++++++++++++----------------------------------------- 1 file changed, 20 insertions(+), 58 deletions(-) diff --git a/src/ui.js b/src/ui.js index 3684cb99bb..c394212c36 100644 --- a/src/ui.js +++ b/src/ui.js @@ -350,7 +350,7 @@ function buildRenderer(givenOpts = {}) { // reference to this md.renderer.rules.image = (tokens, idx, options, env, self) => { - let token = tokens[idx]; + let token = tokens[idx]; let aIndex = token.attrIndex("src"); // Lets say content contains './my-cool-image.png' @@ -359,77 +359,39 @@ function buildRenderer(givenOpts = {}) { if (mdComponents.reg.localLinks.currentDir.test(token.attrGet("src"))) { let rawLink = token.attrGet("src"); rawLink = rawLink.replace(mdComponents.reg.localLinks.currentDir, ""); - // Now that we have the raw link of a local link, we need to handle this - // depending on if the link is local or not - let hasSet = false; - if (validLocalItem) { - let originalSrc = path.resolve(rawLink); + // Now we need to handle links for both the web and locally + // We can do this by first checking if the link resolves locally + if (couldBeLocalItem) { let newSrc = path.resolve(path.dirname(opts.filePath, rawLink)); - if (fs.lstatSync(originalSrc, { throwIfNoEntry: false })?.isFile()) { - // the normal link is already a valid local link to the filesystem - token.attrSet("src", convertPathToLocalProtocol(originalSrc)); - hasSet = true; - } else if (fs.lstatSync(newSrc, { throwIfNoEntry: false })?.isFile()) { - // This link does successfully point to the filesystem after being - // merged with the provided filePath - token.attrSet("src", convertPathToLocalProtocol(newSrc)); - hasSet = true; + if (!fs.lstatSync(newSrc).isFile()) { + token.attrSet("src", newSrc); + } else { + token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); } - } - if (validRootDomain && !hasSet) { - // Attempt to resolve remotely only if this is a valid root domain provided - // and we didn't already set it when checking locally - - // TODO this should not assume that the only image host is GitHub + } else { token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); } } else if (mdComponents.reg.localLinks.rootDir.test(token.attrGet("src"))) { - // tests for any links pointing to the root dir `/` let rawLink = token.attrGet("src"); rawLink = rawLink.replace(mdComponents.reg.localLinks.rootDir, ""); - // Now to handle the possible we or local link resolving - let hasSet = false; - if (validLocalItem) { + // Now to handle the possible web or local link + if (couldBeLocalItem) { const [rootDirectory] = atom.project.relativePath(opts.filePath); - if (fs.lstatSync(src, { throwIfNoEntry: false })?.isFile() && rootDirectory) { + if (!fs.lstatSync(src).isFile() && rootDirectory) { let newSrc = path.join(rootDirectory, rawLink); - token.attrSet("src", convertPathToLocalProtocol(newSrc)); - hasSet = true; + token.attrSet("src", newSrc); + } else { + token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); } - } - if (validRootDomain && !hasSet) { - // TODO again we shouldn't assume this image is on GitHub + } else { token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); - hasSet = true; } } else if (!token.attrGet("src").startsWith("http") && !mdComponents.reg.globalLinks.base64.test(token.attrGet("src"))) { - // This looks like an implicit relative url + // Check for implicit relative urls let rawLink = token.attrGet("src"); - - let hasSet = false; - if (validLocalItem) { - let originalSrc = path.resolve(rawLink); - let newSrc = path.resolve(path.dirname(opts.filePath, rawLink)); - - if (fs.lstatSync(originalSrc, { throwIfNoEntry: false })?.isFile()) { - // the normal link is already a valid local link to the filesystem - token.attrSet("src", convertPathToLocalProtocol(originalSrc)); - hasSet = true; - } else if (fs.lstatSync(newSrc, { throwIfNoEntry: false})?.isFile()) { - // This link does successfully point to the filesystem after being - // merged with the provided filePath - token.attrSet("src", convertPathToLocalProtocol(newSrc)); - hasSet = true; - } - } - if (validRootDomain && !hasSet) { - // TODO again we shouldn't assume this image is one GitHub - token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); - hasSet = true; - } - } else if ([ ".git", ".png", ".jpg", ".jpeg", ".webp"].find(ext => token.attrGet("src").endsWith(ext)) && token.attrGet("src").startsWith("https://github.com") && token.attrGet("src").includes("blob")) { - // Should match images being distributed from GitHub that's using `blob` instead of `raw` - // which will cause images to fail to load. + token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`); + } else if ([".gif", ".png", ".jpg", ".jpeg", ".webp"].find(ext => token.attrGet("src").endsWith(ext)) && token.attrGet("src").startsWith("https://github.com") && token.attrGet("src").includes("blob")) { + // Should match any image being distributed from GitHub that's using `blob` instead of `raw` causing images to not load correctly let rawLink = token.attrGet("src"); token.attrSet("src", rawLink.replace("blob", "raw")); } From c1fa7b1936a8a097ef78088b0bfc8c1a70d54401 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 28 Jan 2024 03:38:54 -0800 Subject: [PATCH 12/17] Revert extra logging --- packages/settings-view/lib/package-readme-view.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/settings-view/lib/package-readme-view.js b/packages/settings-view/lib/package-readme-view.js index b47702dd0b..84653fb1cb 100644 --- a/packages/settings-view/lib/package-readme-view.js +++ b/packages/settings-view/lib/package-readme-view.js @@ -40,7 +40,6 @@ export default class PackageReadmeView { try { this.packageReadme.innerHTML = atom.ui.markdown.render(readme, markdownOpts); } catch(err) { - console.error(err); this.packageReadme.innerHTML = "

Error parsing README

"; } } From c07e97ac93542cf31eac443859e2f9645ff1c625 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 28 Jan 2024 18:29:26 -0800 Subject: [PATCH 13/17] Missed some spots when reverting link transformations --- src/ui.js | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/ui.js b/src/ui.js index c394212c36..0b30c9e7c1 100644 --- a/src/ui.js +++ b/src/ui.js @@ -299,24 +299,14 @@ function buildRenderer(givenOpts = {}) { // Maybe we should emit a warning or deprecation when one is used? // Setup - const validRootDomain = (typeof opts.rootDomain === "string" && opts.rootDomain.length > 1); - const validLocalItem = (typeof opts.filePath === "string" && opts.filePath.length > 1); + const validateRootDomain = () => { + return typeof opts.rootDomain === "string" && opts.rootDomain.length > 1; + }; const cleanRootDomain = () => { // We will also remove any trailing `/` as link resolvers down the line add them in return opts.rootDomain.replace(".git", "").replace(/\/$/, ""); }; - const convertPathToLocalProtocol = (link) => { - // Not sure how this wasn't caught before, but when we resolve a local link, - // such as `D:\pulsar-edit\pulsar\resources\readme.png` two things happen: - // If `sanitize: true` then DOMPurify removes all "\" then the resulting URL - // is invalid, but even worse if `sanitizeAllowUnknownProtocols: false` - // the URL will be removed entirely. - // So we need to convert from a valid local path like above, to a valid - // file protocol link, such as `file:///D:/pulsar-edit/pulsar/resources/readme.png` - link = link.replace(/\\/g, "/"); - link = `file:///${link}`; - return link; - }; + const markdownItOpts = { html: opts.html, @@ -343,14 +333,21 @@ function buildRenderer(givenOpts = {}) { } // Hook up custom rules - if (opts.transformImageLinks) { + if (opts.transformImageLinks && validateRootDomain()) { // Here we will take any links for images provided in the content, and do // our best to ensure they can accurately resolve. - const defaultImageRenderer = md.renderer.rules.image; // We want to keep a - // reference to this + const defaultImageRenderer = md.renderer.rules.image; // We want to keep access to this + + // Determines when we handle links if the item could be a local file or not + let couldBeLocalItem; + if (typeof opts.filePath != "string" || opts.filePath.length < 1) { + couldBeLocalItem = false; + } else { + couldBeLocalItem = true; + } md.renderer.rules.image = (tokens, idx, options, env, self) => { - let token = tokens[idx]; + let token = tokens[idx]; let aIndex = token.attrIndex("src"); // Lets say content contains './my-cool-image.png' @@ -401,7 +398,7 @@ function buildRenderer(givenOpts = {}) { }; } - if (validRootDomain && opts.transformNonFqdnLinks) { + if (validateRootDomain() && opts.transformNonFqdnLinks) { md.core.ruler.after("inline", "fix-links", (state) => { state.tokens.forEach((blockToken) => { if (blockToken.type === "inline" && blockToken.children) { From 4b139158595850bcf4fc0674cd509b49ea81f70b Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 29 Jan 2024 17:13:05 -0800 Subject: [PATCH 14/17] Update specs to new defaults --- spec/ui-spec.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/spec/ui-spec.js b/spec/ui-spec.js index 8ad5e0e667..5b4c230b14 100644 --- a/spec/ui-spec.js +++ b/spec/ui-spec.js @@ -8,28 +8,41 @@ describe("Renders Markdown", () => { }); describe("transforms links correctly", () => { + const mdOpts = { + transformImageLinks: true, + transformNonFqdnLinks: true + }; + it("makes no changes to a fqdn link", () => { - expect(atom.ui.markdown.render("[Hello World](https://github.com)")) + expect(atom.ui.markdown.render("[Hello World](https://github.com)", mdOpts)) .toBe('

Hello World

\n'); }); it("resolves package links to pulsar", () => { - expect(atom.ui.markdown.render("[Hello](https://atom.io/packages/hey-pane)")) + expect(atom.ui.markdown.render("[Hello](https://atom.io/packages/hey-pane)", mdOpts)) .toBe('

Hello

\n'); }); it("resolves atom links to web archive", () => { - expect(atom.ui.markdown.render("[Hello](https://flight-manual.atom.io/some-docs)")) + expect(atom.ui.markdown.render("[Hello](https://flight-manual.atom.io/some-docs)", mdOpts)) .toBe('

Hello

\n'); }); it("resolves incomplete local links", () => { expect(atom.ui.markdown.render( "[Hello](./readme.md)", - { rootDomain: "https://github.com/pulsar-edit/pulsar" } + { + rootDomain: "https://github.com/pulsar-edit/pulsar", + transformImageLinks: true, + transformNonFqdnLinks: true + } )).toBe('

Hello

\n'); }); it("resolves incomplete root links", () => { expect(atom.ui.markdown.render( "[Hello](/readme.md)", - { rootDomain: "https://github.com/pulsar-edit/pulsar" } + { + rootDomain: "https://github.com/pulsar-edit/pulsar", + transformImageLinks: true, + transformNonFqdnLinks: true + } )).toBe('

Hello

\n'); }); }); From 09ea8c370faca3125f3722d724165ca70cb332e8 Mon Sep 17 00:00:00 2001 From: confused_techie Date: Thu, 15 Feb 2024 08:11:23 -0800 Subject: [PATCH 15/17] Update src/ui.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maurício Szabo --- src/ui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui.js b/src/ui.js index 0b30c9e7c1..976a5ee75e 100644 --- a/src/ui.js +++ b/src/ui.js @@ -256,8 +256,8 @@ function render(content, givenOpts = {}, mdInstance) { * `divWrap` option. * @param {boolean} givenOpts.transformImageLinks - If links to images should * attempted to be resolved. - * @param {boolean} givenOpts.transformNonFqdnLinks - If non-FQDN links should be - * resolved. + * @param {boolean} givenOpts.transformNonFqdnLinks - If non fully qualified + * domain name links should be resolved. * @param {boolean} givenOpts.transformAtomLinks - If links to Atom pages should * resolved to the Pulsar equivolant. * @param {string} givenOpts.rootDomain - The root URL of the online resource. From 65390468543e5c995b6067292fd7d4dddddeffb1 Mon Sep 17 00:00:00 2001 From: confused_techie Date: Thu, 15 Feb 2024 08:11:29 -0800 Subject: [PATCH 16/17] Update src/ui.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maurício Szabo --- src/ui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui.js b/src/ui.js index 976a5ee75e..8bb4a23ce1 100644 --- a/src/ui.js +++ b/src/ui.js @@ -254,8 +254,8 @@ function render(content, givenOpts = {}, mdInstance) { * `disabled` option. * @param {boolean} givenOpts.taskCheckboxDivWrap - Controls `markdown-it-task-checkbox` * `divWrap` option. - * @param {boolean} givenOpts.transformImageLinks - If links to images should - * attempted to be resolved. + * @param {boolean} givenOpts.transformImageLinks - If it should attempt to + * resolve links to images. * @param {boolean} givenOpts.transformNonFqdnLinks - If non fully qualified * domain name links should be resolved. * @param {boolean} givenOpts.transformAtomLinks - If links to Atom pages should From aa7ea65c8d8a5c3fdf42b19e523e3dc5feac59c2 Mon Sep 17 00:00:00 2001 From: confused_techie Date: Thu, 15 Feb 2024 08:11:44 -0800 Subject: [PATCH 17/17] Update src/ui.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maurício Szabo --- src/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui.js b/src/ui.js index 8bb4a23ce1..a43370e0fb 100644 --- a/src/ui.js +++ b/src/ui.js @@ -259,7 +259,7 @@ function render(content, givenOpts = {}, mdInstance) { * @param {boolean} givenOpts.transformNonFqdnLinks - If non fully qualified * domain name links should be resolved. * @param {boolean} givenOpts.transformAtomLinks - If links to Atom pages should - * resolved to the Pulsar equivolant. + * resolved to the Pulsar equivalent. * @param {string} givenOpts.rootDomain - The root URL of the online resource. * Used when resolving links. * @param {string} givenOpts.filePath - The path to the local resource.