diff --git a/CHANGELOG.md b/CHANGELOG.md index f925845..484df1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,21 @@ # Change Log +### 0.12.7 + +- [#329](https://github.com/PDConSec/vsc-print/issues/329) - syntax coloured source in Markdown fenced blocks +- [#328](https://github.com/PDConSec/vsc-print/issues/328) - user supplied CSS + ### 0.12.5 - [#326](https://github.com/PDConSec/vsc-print/issues/326) - support for Kroki - this unifies the rendering of a large number of diagram notations, notably Mermaid and C4 - Kroki is server based. In line with our philosophy of off-line operation, there is a setting for the URL of the Kroki server and a link in the setting description to instructions for setting up a local installation of Kroki. -- # Reworked Katex integration to support +- [#324](https://github.com/PDConSec/vsc-print/issues/324) - Reworked Katex integration to support - `$$` fenced display blocks - - [#324](https://github.com/PDConSec/vsc-print/issues/324) `$%...%$` inline equations + - `$%...%$` inline equations - # MHCHEM equations - - [#327](https://github.com/PDConSec/vsc-print/issues/327) - separate settings for visibility of print and preview icons - - localised several recently added settings +- [#327](https://github.com/PDConSec/vsc-print/issues/327) - separate settings for visibility of print and preview icons +- localised several recently added settings - [#287](https://github.com/PDConSec/vsc-print/issues/287) = new scheme [none] is black and white ### 0.12.3 diff --git a/README.md b/README.md index 66310a6..6826b07 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,14 @@ Kroki is server-based. Normally we won't do anything that can't work offline, bu So while there was a drop in functionality for 0.12.3, with this release you can use the following: -| | | | | | | | -|--------------|------------------|-----------|------------|---------|--------|------------| -| BlockDiag | BPMN | Bytefield | SeqDiag | ActDiag | NwDiag | PacketDiag | -| RackDiag | C4 with PlantUML| D2 | DBML | Ditaa | Erd | Excalidraw | -| GraphViz | KaTeX | Mermaid | MHCHEM | Nomnoml | Pikchr | PlantUML | -| SmilesDrawer | Structurizr | Svgbob | Symbolator | Tikz | UMLet | Vega | -| Vega-lite | WaveDrom | WireViz |   |   |   |   | +| | | | | | | +|------------|------------|------------------|--------------|-------------|----------| +| BlockDiag | BPMN | Bytefield | SeqDiag | ActDiag | NwDiag | +| PacketDiag | RackDiag | C4 with PlantUML | D2 | DBML | Ditaa | +| Erd | Excalidraw | GraphViz | KaTeX | Mermaid | MHCHEM | +| Nomnoml | Pikchr | PlantUML | SmilesDrawer | Structurizr | Svgbob | +| Symbolator | Tikz | UMLet | Vega | Vega-lite | WaveDrom | +| WireViz | | | | | | ## Cross-platform printing diff --git a/doc/SDK.md b/doc/SDK.md index 4b3e3e5..c2e3937 100644 --- a/doc/SDK.md +++ b/doc/SDK.md @@ -40,7 +40,7 @@ Here's something meatier - this is how the default renderer applies syntax-colou After that a line numbers are added, or not, depending on settings. Finally optional word breaks are inserted to improve the breaking of long spans of code that lack natural opportunities. -```ts +```typescript export function getBodyHtml(raw: string, languageId: string, options?:any): string { let renderedCode = ""; try { diff --git a/package.json b/package.json index 1c78be7..6ea736f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-print", "displayName": "Print", "description": "Rendered Markdown, coloured code.", - "version": "0.12.5", + "version": "0.12.7", "icon": "assets/vscode-print-128.png", "author": { "name": "Peter Wone", @@ -263,6 +263,27 @@ }, "description": "%print.folder.include.description%" }, + "print.stylesheets.markdown": { + "type": "array", + "items": { + "type": "string" + }, + "description": "%print.stylesheets%" + }, + "print.stylesheets.plaintext": { + "type": "array", + "items": { + "type": "string" + }, + "description": "%print.stylesheets%" + }, + "print.stylesheets.sourcecode": { + "type": "array", + "items": { + "type": "string" + }, + "description": "%print.stylesheets%" + }, "print.folder.exclude": { "default": [ "{bin,obj,out}", @@ -656,4 +677,4 @@ "publisherId": "a803a703-65fb-4fa9-955a-c9259bf2560d", "isPreReleaseVersion": false } -} +} \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index 1302d10..93898f9 100644 --- a/package.nls.json +++ b/package.nls.json @@ -31,6 +31,7 @@ "print.lineSpacing.double": "Double spaced", "print.preview.description": "Preview the rendered document in a brower", "print.renderMarkdown.description": "Render Markdown as HTML when printing", + "print.stylesheets": "URLs or paths for extra CSS stylesheets. For workspace relative paths, use 'workspace.resource/path/to/your.css'. Otherwise, paths should be relative to the base document ('./my.css' would be in the same folder as the document), or absolute (https://...)", "print.logLevel.description": "Log level 'Error' will log only errors (very few entries) whereas 'info' will log errors, warnings and info. 'Debug' is good for troubleshooting. 'Silly' will log absolutely everything but the log file will grow fast.", "print.krokiUrl.markdownDescription": "URL for Kroki diagram rendering service. To get you going this defaults to the internet service but you should [install your own](https://kroki.io/#install) as soon as possible." } \ No newline at end of file diff --git a/src/css/settings.css b/src/css/settings.css index 9634207..5371496 100644 --- a/src/css/settings.css +++ b/src/css/settings.css @@ -6,7 +6,9 @@ body { } .hljs { - background-color: unset; + background-color: transparent; + font-family: FONT_FAMILY; + font-size: FONT_SIZE; } .line-number, @@ -27,3 +29,10 @@ pre.plaintext { padding-bottom: 3px; white-space: pre-wrap; } + +.code-box { + background-color: #f8f8f8; + border-radius: 0.5em; + border: thin solid silver; + white-space: pre-wrap; +} \ No newline at end of file diff --git a/src/renderers/html-renderer-markdown.ts b/src/renderers/html-renderer-markdown.ts index e7ca926..6bb79ea 100644 --- a/src/renderers/html-renderer-markdown.ts +++ b/src/renderers/html-renderer-markdown.ts @@ -45,10 +45,13 @@ export async function getBodyHtml(generatedResources: Map { + const userSuppliedCssUrls: string[] = vscode.workspace.getConfiguration("print.stylesheets").markdown; const cssUriStrings = [ "bundled/default-markdown.css", - "bundled/settings.css", "bundled/katex.css", + "bundled/colour-scheme.css", + ...userSuppliedCssUrls, + "bundled/settings.css"//ensure settings are always last so they take precedence ]; return cssUriStrings; } diff --git a/src/renderers/html-renderer-plaintext.ts b/src/renderers/html-renderer-plaintext.ts index 98df9ec..7ef98fa 100644 --- a/src/renderers/html-renderer-plaintext.ts +++ b/src/renderers/html-renderer-plaintext.ts @@ -2,13 +2,15 @@ import { escapeHtml } from "markdown-it/lib/common/utils"; import * as vscode from 'vscode'; import { IResourceDescriptor } from "./IResourceDescriptor"; -export async function getBodyHtml(generatedResources: Map, raw: string): Promise { - return `
\n${escapeHtml(raw)}\n
`; +export async function getBodyHtml(_: Map, raw: string): Promise { + return `
\n${escapeHtml(raw)}\n
`; } export function getCssUriStrings(uri: vscode.Uri): Array { - return [ - "bundled/default.css", - "bundled/settings.css", - ]; + const userSuppliedCssUrls: string[] = vscode.workspace.getConfiguration("print.stylesheets").plaintext; + return [ + "bundled/default.css", + ...userSuppliedCssUrls, + "bundled/settings.css", + ]; } diff --git a/src/renderers/html-renderer-sourcecode.ts b/src/renderers/html-renderer-sourcecode.ts index e5dbaa2..2db6dd7 100644 --- a/src/renderers/html-renderer-sourcecode.ts +++ b/src/renderers/html-renderer-sourcecode.ts @@ -4,114 +4,116 @@ import { logger } from '../logger'; import hljs from 'highlight.js'; const resources = new Map(); resources.set("default.css", { - content: require("highlight.js/styles/default.css").default.toString(), - mimeType: "text/css; charset=utf-8;" + content: require("highlight.js/styles/default.css").default.toString(), + mimeType: "text/css; charset=utf-8;" }); resources.set("line-numbers.css", { - content: require("../css/line-numbers.css").default.toString(), - mimeType: "text/css; charset=utf-8;" + content: require("../css/line-numbers.css").default.toString(), + mimeType: "text/css; charset=utf-8;" }); export async function getBodyHtml(generatedResources: Map, raw: string, languageId: string, options?: any): Promise { - let renderedCode = ""; - try { - try { - renderedCode = hljs.highlight(raw, { language: languageId }).value; - if (!renderedCode.includes('"hljs-keyword"')) { - logger.warn(`Language identifier "${languageId}" could not be honoured. Autodetecting.`); - renderedCode = hljs.highlightAuto(raw).value; - } - } - catch (err) { - logger.warn(`Language identifier "${languageId}" could not be honoured. Autodetecting.`); - renderedCode = hljs.highlightAuto(raw).value; - } - if (languageId === "css") { - renderedCode = addCssColourSwatches(renderedCode); - } - renderedCode = fixMultilineSpans(renderedCode); - const printConfig = vscode.workspace.getConfiguration("print"); - const pattern = /((?:(?:\w{40}|[\])},;]))(?![^<>]*>))/g; - const replacement = "$1"; - logger.debug(`Line numbering: ${printConfig.lineNumbers} (resolves to ${options.lineNumbers})`) - if (options.lineNumbers) { - renderedCode = renderedCode - .split("\n") - .map(line => line || " ") - .map((line, i) => `${options.startLine + i}${line.replace(/(\w{20})/g, "$1­")}`) - .join("\n") - .replace("\n", "") - ; - } else { - renderedCode = renderedCode - .split("\n") - .map(line => line || " ") - .map((line) => `${line.replace(pattern, replacement)}`) - .join("\n") - .replace("\n", "") - ; - } - } catch (err) { - logger.error(`Markdown could not be rendered\n${err}`); - renderedCode = "
Could not render this file."; - } - return `\n${renderedCode}\n
`; + let renderedCode = ""; + try { + try { + renderedCode = hljs.highlight(raw, { language: languageId }).value; + if (!renderedCode.includes('"hljs-keyword"')) { + logger.warn(`Language identifier "${languageId}" could not be honoured. Autodetecting.`); + renderedCode = hljs.highlightAuto(raw).value; + } + } + catch (err) { + logger.warn(`Language identifier "${languageId}" could not be honoured. Autodetecting.`); + renderedCode = hljs.highlightAuto(raw).value; + } + if (languageId === "css") { + renderedCode = addCssColourSwatches(renderedCode); + } + renderedCode = fixMultilineSpans(renderedCode); + const printConfig = vscode.workspace.getConfiguration("print"); + const pattern = /((?:(?:\w{40}|[\])},;]))(?![^<>]*>))/g; + const replacement = "$1"; + logger.debug(`Line numbering: ${printConfig.lineNumbers} (resolves to ${options.lineNumbers})`) + if (options.lineNumbers) { + renderedCode = renderedCode + .split("\n") + .map(line => line || " ") + .map((line, i) => `${options.startLine + i}${line.replace(/(\w{20})/g, "$1­")}`) + .join("\n") + .replace("\n", "") + ; + } else { + renderedCode = renderedCode + .split("\n") + .map(line => line || " ") + .map((line) => `${line.replace(pattern, replacement)}`) + .join("\n") + .replace("\n", "") + ; + } + } catch (err) { + logger.error(`Markdown could not be rendered\n${err}`); + renderedCode = "
Could not render this file."; + } + return `\n${renderedCode}\n
`; } export function getCssUriStrings(uri: vscode.Uri): Array { - return [ - "bundled/default.css", - "bundled/line-numbers.css", - "bundled/colour-scheme.css", - "bundled/settings.css", - ]; + const userSuppliedCssUrls: string[] = vscode.workspace.getConfiguration("print.stylesheets").sourcecode; + return [ + "bundled/default.css", + "bundled/line-numbers.css", + "bundled/colour-scheme.css", + ...userSuppliedCssUrls, + "bundled/settings.css", + ]; } export function getResource(name: string): IResourceDescriptor { - return resources.get(name)!; + return resources.get(name)!; } function fixMultilineSpans(text: string): string { - let classes: string[] = []; + let classes: string[] = []; - // since this code runs on simple, well-behaved, escaped HTML, we can just - // use regex matching for the span tags and classes + // since this code runs on simple, well-behaved, escaped HTML, we can just + // use regex matching for the span tags and classes - // first capture group is if it's a closing tag, second is tag attributes - const spanRegex = /<(\/?)span(.*?)>/g; - // https://stackoverflow.com/questions/317053/regular-expression-for-extracting-tag-attributes - // matches single html attribute, first capture group is attr name and second is value - const tagAttrRegex = /(\S+)=["']?((?:.(?!["']?\s+(?:\S+)=|\s*\/?[>"']))+.)["']?/g; + // first capture group is if it's a closing tag, second is tag attributes + const spanRegex = /<(\/?)span(.*?)>/g; + // https://stackoverflow.com/questions/317053/regular-expression-for-extracting-tag-attributes + // matches single html attribute, first capture group is attr name and second is value + const tagAttrRegex = /(\S+)=["']?((?:.(?!["']?\s+(?:\S+)=|\s*\/?[>"']))+.)["']?/g; - return text.split("\n").map(line => { - const pre = classes.map(classVal => ``); + return text.split("\n").map(line => { + const pre = classes.map(classVal => ``); - let spanMatch; - spanRegex.lastIndex = 0; // exec maintains state which we need to reset - while ((spanMatch = spanRegex.exec(line)) !== null) { - if (spanMatch[1] !== "") { - classes.pop(); - continue; - } - let attrMatch; - tagAttrRegex.lastIndex = 0; - while ((attrMatch = tagAttrRegex.exec(spanMatch[2])) !== null) { - if (attrMatch[1].toLowerCase().trim() === "class") { - classes.push(attrMatch[2]); - } - } - } + let spanMatch; + spanRegex.lastIndex = 0; // exec maintains state which we need to reset + while ((spanMatch = spanRegex.exec(line)) !== null) { + if (spanMatch[1] !== "") { + classes.pop(); + continue; + } + let attrMatch; + tagAttrRegex.lastIndex = 0; + while ((attrMatch = tagAttrRegex.exec(spanMatch[2])) !== null) { + if (attrMatch[1].toLowerCase().trim() === "class") { + classes.push(attrMatch[2]); + } + } + } - return `${pre.join("")}${line}${"".repeat(classes.length)}`; - }).join("\n"); + return `${pre.join("")}${line}${"".repeat(classes.length)}`; + }).join("\n"); } function addCssColourSwatches(text: string): string { - return text.replace( - /(:\s*)([#A-Za-z][A-Za-z0-9]+)/g, - '$1 $2' - ).replace( - /(.*<\/span>\s*:\s*)(aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|grey|green|greenyellow|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|navyblue|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen)(?:\s*);/gm, - '$1 $2' - ); + return text.replace( + /(:\s*)([#A-Za-z][A-Za-z0-9]+)/g, + '$1 $2' + ).replace( + /(.*<\/span>\s*:\s*)(aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|grey|green|greenyellow|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|navyblue|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen)(?:\s*);/gm, + '$1 $2' + ); } diff --git a/src/renderers/processMarkdown.ts b/src/renderers/processMarkdown.ts index ea579ca..ac306f8 100644 --- a/src/renderers/processMarkdown.ts +++ b/src/renderers/processMarkdown.ts @@ -7,7 +7,9 @@ import crypto from "crypto"; import { deflate } from "pako"; import * as vscode from 'vscode'; import 'katex/dist/contrib/mhchem'; +import hljs from 'highlight.js'; +const HIGHLIGHTJS_LANGS = hljs.listLanguages().map(s => s.toUpperCase()); const KROKI_SUPPORT = ["BLOCKDIAG", "BPMN", "BYTEFIELD", "SEQDIAG", "ACTDIAG", "NWDIAG", "PACKETDIAG", "RACKDIAG", "C4", "D2", "DBML", "DITAA", "ERD", "EXCALIDRAW", "GRAPHVIZ", "MERMAID", "NOMNOML", "PIKCHR", "PLANTUML", "STRUCTURIZR", "SVGBOB", "SYMBOLATOR", "TIKZ", "UMLET", "VEGA", "VEGA-LITE", "WAVEDROM", "WIREVIZ"]; // import { fixFalsePrecision, formatXml, applyDiagramStyle, stripPreamble } from './svg-tools'; @@ -86,8 +88,13 @@ export async function processFencedBlocks(defaultConfig: any, raw: string, gener updatedTokens.push({ block: true, type: "code", raw: token.raw, text: resolvedConfig }); break; //#endregion - default: //unhandled passthrough - updatedTokens.push(token); + default: + if (HIGHLIGHTJS_LANGS.includes(LANG)) { + const codeBlock = `
\n\n${hljs.highlight(token.lang, token.text).value}\n\n
\n`; + updatedTokens.push({ block: true, type: "html", raw: token.raw, text: codeBlock }); + } else { //unhandled passthrough + updatedTokens.push(token); + } break; } }