From 4bb213e148659fdfc1105e2f8be66785042e39c0 Mon Sep 17 00:00:00 2001 From: Robert McGovern Date: Thu, 2 Feb 2023 23:46:44 +0000 Subject: [PATCH] work in progress. basic arguments handled in njk and md, need to do liquid and handling lineNumbersStart, highlight lines --- .eleventy.js | 83 +++++++++------ README.md | 39 ++++++- demo/test-liquid.liquid | 36 +++++++ demo/test-markdown.md | 145 +++++++++++++++++++++++++- demo/test-nunjucks.njk | 25 +++++ demo/test.css | 22 ++++ src/HighlightPairedShortcode.js | 43 ++++---- src/LiquidHighlightTag.js | 32 ++++-- src/markdownSyntaxHighlightOptions.js | 45 +++----- src/parseSyntaxArguments.js | 111 ++++++++++++++++++++ 10 files changed, 479 insertions(+), 102 deletions(-) create mode 100644 src/parseSyntaxArguments.js diff --git a/.eleventy.js b/.eleventy.js index c8c787b..3af4272 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -1,59 +1,78 @@ const pkg = require("./package.json"); -const Prism = require("prismjs"); const hasTemplateFormat = require("./src/hasTemplateFormat"); const HighlightPairedShortcode = require("./src/HighlightPairedShortcode"); const LiquidHighlightTag = require("./src/LiquidHighlightTag"); -const CharacterWrap = require("./src/CharacterWrap"); +// const CharacterWrap = require("./src/CharacterWrap"); const markdownPrismJs = require("./src/markdownSyntaxHighlightOptions"); module.exports = { - initArguments: { Prism }, - configFunction: function(eleventyConfig, options = {}) { + // initArguments: { Prism }, + configFunction: function (eleventyConfig, options = {}) { try { eleventyConfig.versionCheck(pkg["11ty"].compatibility); - } catch(e) { - console.log( `WARN: Eleventy Plugin (${pkg.name}) Compatibility: ${e.message}` ); + } catch (e) { + console.log( + `WARN: Eleventy Plugin (${pkg.name}) Compatibility: ${e.message}` + ); } - options = Object.assign({ - alwaysWrapLineHighlights: false, - // eligible to change the default to \n in a new major version. - lineSeparator: "
", - preAttributes: {}, - codeAttributes: {} - }, options); + options = Object.assign( + { + theme: "onedark", + lineNumbers: false, + /* lineNumbersStyle: "table",*/ /* "table" or "inline" */ + alwaysWrapLineHighlights: false, + // eligible to change the default to \n in a new major version. + lineSeparator: "
", + preAttributes: {}, + codeAttributes: { + theme: "onedark", + }, + }, + options + ); - // TODO hbs? - if( hasTemplateFormat(options.templateFormats, "liquid") ) { + if (hasTemplateFormat(options.templateFormats, "liquid")) { eleventyConfig.addLiquidTag("highlight", (liquidEngine) => { - // {% highlight js 0 2 %} + // {% highlight js 0,2 %} let highlight = new LiquidHighlightTag(liquidEngine); return highlight.getObject(options); }); } - if( hasTemplateFormat(options.templateFormats, "njk") ) { - eleventyConfig.addPairedNunjucksShortcode("highlight", (content, args) => { - // {% highlight "js 0 2-3" %} - let [language, ...highlightNumbers] = args.split(" "); - return HighlightPairedShortcode(content, language, highlightNumbers.join(" "), options); - }); + if (hasTemplateFormat(options.templateFormats, "njk")) { + eleventyConfig.addPairedNunjucksShortcode( + "highlight", + (content, args) => { + // {% highlight "js 0,2-3" %} + return HighlightPairedShortcode(content, args, options); + } + ); } - if( hasTemplateFormat(options.templateFormats, "md") ) { + if (hasTemplateFormat(options.templateFormats, "md")) { // ```js/0,2-3 eleventyConfig.addMarkdownHighlighter(markdownPrismJs(options)); } - if( hasTemplateFormat(options.templateFormats, "11ty.js") ) { - eleventyConfig.addJavaScriptFunction("highlight", (language, content, highlight1, highlight2) => { - let highlightLines = [highlight1, highlight2].filter(entry => entry).join(" "); - let result = HighlightPairedShortcode(content, language, highlightLines, options); - return result; - }); - } - } + // if (hasTemplateFormat(options.templateFormats, "11ty.js")) { + // eleventyConfig.addJavaScriptFunction( + // "highlight", + // (language, content, highlight1, highlight2) => { + // let highlightLines = [highlight1, highlight2] + // .filter((entry) => entry) + // .join(" "); + // let result = HighlightPairedShortcode( + // content, + // args, + // options + // ); + // return result; + // } + // ); + // } + }, }; module.exports.pairedShortcode = HighlightPairedShortcode; -module.exports.CharacterWrap = CharacterWrap; +// module.exports.CharacterWrap = CharacterWrap; diff --git a/README.md b/README.md index c55781d..7ee7470 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,41 @@

eleventy Logo

-# eleventy-plugin-syntaxhighlight 🕚⚡️🎈🐀 +# eleventy-plugin-syntaxhighlight-chroma -A pack of [Eleventy](https://github.com/11ty/eleventy) plugins for syntax highlighting. No browser/client JavaScript here, these highlight transformations are all done at build-time. +A module for handling syntax highlighting in [Eleventy](https://github.com/11ty/eleventy) using the [Chroma](https://github.com/alecthomas/chroma); a syntax highlighter written in Go. There is no browser/client JavaScript required, the highlight transformations are all done at build-time. -## Read the [Full Documentation on 11ty.dev](https://www.11ty.dev/docs/plugins/syntaxhighlight/) +I am making using of the [chroma-highlight](https://github.com/krymel/chroma-highlight) NPM package to include `Chroma` support. (It handles downloading the required binary for the platform you are working on). +This module/plugin took the 11ty plugin [eleventy-plugin-syntaxhighlight](https://github.com/11ty/eleventy-plugin-syntaxhighlight) as the basis. + +## Supported args in code blocks + +The first argument is always expected to be the language, at present there is no bugout/fail if a language is not provided first. (Nor in the original plugin) + +For Markdown, separate arguments with a `/`, this seems to be hard coded somewhere and I haven't a workaround yet. For `liquid` and `njk` use spaces (` `) to separate arguments. + +Arguments: + +- `lineNumbers` will add line numbers starting from 1 for each code block. +- `lineNumbersStyle` if `table` is used, then code block will use a table to make it easier to drag and select the code. i.e `lineNumberStyle=table` +- `lineNumbersStart` the number to start the line number count from. i.e `lineNumbersStart=200` + + +## Supported `options` in eleventy config + +You can specify some arguments in the options object in `.eleventy.js` config. ~~Options are considered defaults, and can be overriden by codeblock arguments.~~**TODO** + +Example of `options` object + +``` +options +{ + theme: 'onedark', + lineNumbers: false, +} +``` + +Theme can be set to one of these [themes](https://xyproto.github.io/splash/docs/all.html). If no theme is specified, then `xcode-dark` is used. + +- `lineNumbers` will add line numbers starting from 1 for each code block. +- `lineNumbersStyle` if `table` is used, then code block will use a table to make it easier to drag and select the code. diff --git a/demo/test-liquid.liquid b/demo/test-liquid.liquid index d714e2c..db39646 100644 --- a/demo/test-liquid.liquid +++ b/demo/test-liquid.liquid @@ -8,12 +8,16 @@ + + Just JS {% highlight js %} function myFunction() { return true; } {% endhighlight %} +Just JS multiline + {% highlight js %} let multilineString = ` this is the first line @@ -22,6 +26,7 @@ let multilineString = ` `; {% endhighlight %} +JS + Linehighlight {% highlight js 1,3 %} let multilineString = ` this is the first line @@ -29,5 +34,36 @@ let multilineString = ` this is the last line `; {% endhighlight %} + +Swift + show lineNumbers + +{% highlight swift lineNumbers %} +let multilineString = ` + this is the first line + this is the middle line + this is the last line +`; +{% endhighlight %} + +Swift + show lineNumbers + highlight 1 & 3 + +{% highlight swift lineNumbers 1,3 %} +let multilineString = ` + this is the first line + this is the middle line + this is the last line +`; +{% endhighlight %} + +Swift + show lineNumbers + highlight 1 & 3 + +{% highlight swift lineNumbers 1,3 table %} +let multilineString = ` + this is the first line + this is the middle line + this is the last line +`; +{% endhighlight %} + diff --git a/demo/test-markdown.md b/demo/test-markdown.md index 15ba5e8..4fc8cbd 100644 --- a/demo/test-markdown.md +++ b/demo/test-markdown.md @@ -4,10 +4,20 @@ - + + Just `curious` what + +No lang +``` +function myFunction() { + return true; +} +``` + +Langs ```ts function myFunction() { return true; @@ -44,6 +54,7 @@ let multilineString = ` `; ``` +Highlight 1 & 3 ```js/1,3 let multilineString = ` this is the first line @@ -52,11 +63,143 @@ let multilineString = ` `; ``` +Highlight 1, 3-6 + +```js/1,3-6 +let multilineString = ` + this is the first line + this is the second line + this is the third line + this is the fourth line + this is the fifth line + this is the sixth line + this is the seventh line + this is the eighth line +`; +``` + +Highlight 1, 3-6 + +```js/1,3:6 +let multilineString = ` + this is the first line + this is the second line + this is the third line + this is the fourth line + this is the fifth line + this is the sixth line + this is the seventh line + this is the eighth line +`; +``` + +Line numbers and highlight +Highlight 1, 3-6 + +```js/1,3:6/lineNumbers +let multilineString = ` + this is the first line + this is the second line + this is the third line + this is the fourth line + this is the fifth line + this is the sixth line + this is the seventh line + this is the eighth line +`; +``` + +Line numbers, highlight, base number 200, table +Highlight 1, 3-6 + +```js/1,3:6/lineNumbers/table/lineNumbersStart=200 +let multilineString = ` + this is the first line + this is the second line + this is the third line + this is the fourth line + this is the fifth line + this is the sixth line + this is the seventh line + this is the eighth line +`; +``` + +Line numbers, highlight, base number 200, table +Highlight 1, 3-6, separated spaces (won't work) + +```swift 1,3:6 lineNumbers table lineNumbersStart=200 +let multilineString = ` + this is the first line + this is the second line + this is the third line + this is the fourth line + this is the fifth line + this is the sixth line + this is the seventh line + this is the eighth line +`; +``` + +## Other arguments + +Table +```js/table +function myFunction() { + return true; +} +``` + +linenums +```js/lineNumbers +function myFunction() { + return true; +} +``` + ## Scrollbar ```js import { aReallyLongFunctionNameThatCouldBeLongerButThisShouldBeLongEnoughByNowHopefully as anEvenLongerFunctionNameWithMoreCharactersThanCouldBeImaginedByAnyOnePersonInThisEntireWorldOfPeopleThatOneMightKnowAtLeastThatIsWhatIsTheorizedByThisLongName } from 'wow-this-is-so-long-you-might-need-a-scrollbar-to-see-it.long-ol-file-extension-that-should-not-be-this-long-on-a-real-site-but-this-is-to-demonstrate-the-accessibility-of-tabindex-and-scrollbars.js'; ``` +## CSS +```css +pre { + display: block; + padding: 0.75rem 1rem; + line-height: 1.5; + + overflow-x: auto; + background-color: #eee; + font-size: 1em; /*14px /16*/ + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; + + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + + background-color: #272822; + color: #fff; +} + +:not(pre) > code[class*="language-"] { + padding: 0.1em 0.3em; + border-radius: 0.3em; + white-space: normal; + } + + .token.comment, + .token.prolog, + .token.doctype, + .token.cdata { + color: #8da1b9; + } +``` + diff --git a/demo/test-nunjucks.njk b/demo/test-nunjucks.njk index 02513be..c569fc7 100644 --- a/demo/test-nunjucks.njk +++ b/demo/test-nunjucks.njk @@ -9,6 +9,8 @@ +Just `curious` what + {% highlight "js" %} function myFunction() { return true; @@ -70,5 +72,28 @@ function myFunction() { return true; } {% endhighlight %} + +lineNumbers +{% highlight "ruby lines lineNumbers" %} +function myFunction() { + return true; +} +{% endhighlight %} + +table +{% highlight "ruby lines table" %} +function myFunction() { + return true; +} +{% endhighlight %} + +lineNumbers + table +{% highlight "ruby lines table lineNumbers" %} +function myFunction() { + return true; +} + +{% endhighlight %} + diff --git a/demo/test.css b/demo/test.css index 5ab54a7..326d7bc 100644 --- a/demo/test.css +++ b/demo/test.css @@ -13,3 +13,25 @@ .highlight-line .highlight-line:not(:last-child) { min-width: 0; } + +pre { + display: block; + padding: 0.75rem 1rem; + line-height: 1.5; + + overflow-x: auto; + background-color: #eee; + font-size: 1em; /*14px /16*/ + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; + + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + + background-color: #272822; + color: #fff; +} diff --git a/src/HighlightPairedShortcode.js b/src/HighlightPairedShortcode.js index cb79a6c..23f347b 100644 --- a/src/HighlightPairedShortcode.js +++ b/src/HighlightPairedShortcode.js @@ -1,34 +1,27 @@ -const Prism = require("prismjs"); -const PrismLoader = require("./PrismLoader"); -const HighlightLinesGroup = require("./HighlightLinesGroup"); -const getAttributes = require("./getAttributes"); +const Chroma = require("chroma-highlight"); +const parseSyntaxArguments = require("./parseSyntaxArguments"); -module.exports = function (content, language, highlightNumbers, options = {}) { - // default to on - if(options.trim === undefined || options.trim === true) { - content = content.trim(); +module.exports = function (content, args, options = {}) { + // No args, so don't know language, drop out + if (!args) { + return content; } let highlightedContent; - if( language === "text" ) { - highlightedContent = content; - } else { - highlightedContent = Prism.highlight(content, PrismLoader(language), language); + + if (options.trim === undefined || options.trim === true) { + content = content.trim(); } - let group = new HighlightLinesGroup(highlightNumbers); - let lines = highlightedContent.split(/\r?\n/); - lines = lines.map(function(line, j) { - if(options.alwaysWrapLineHighlights || highlightNumbers) { - let lineContent = group.getLineMarkup(j, line); - return lineContent; - } - return line; - }); + if (args === "text") { + highlightedContent = content; + } else { + const parsedArgs = parseSyntaxArguments(args, options); - const context = { content: content, language: language, options: options }; - const preAttributes = getAttributes(options.preAttributes, context); - const codeAttributes = getAttributes(options.codeAttributes, context); + let opts = `--formatter html --html-only --html-inline-styles ${parsedArgs} `; - return `` + lines.join(options.lineSeparator || "
") + ""; + highlightedContent = Chroma.highlight(content, opts); + } + + return highlightedContent; }; diff --git a/src/LiquidHighlightTag.js b/src/LiquidHighlightTag.js index 26eb7ce..10f364d 100644 --- a/src/LiquidHighlightTag.js +++ b/src/LiquidHighlightTag.js @@ -1,4 +1,7 @@ const HighlightPairedShortcode = require("./HighlightPairedShortcode"); +const Chroma = require("chroma-highlight"); +const parseSyntaxArguments = require("./parseSyntaxArguments"); +const getAttributes = require("./getAttributes"); class LiquidHighlightTag { constructor(liquidEngine) { @@ -6,9 +9,12 @@ class LiquidHighlightTag { } getObject(options = {}) { - let ret = function(highlighter) { + let ret = function (highlighter) { return { - parse: function(tagToken, remainTokens) { + parse: function (tagToken, remainTokens) { + console.log(">>LIQIUD"); + console.log(tagToken.args); + console.log("< { + .on("token", (token) => { if (token.name === "endhighlight") { stream.stop(); } else { this.tokens.push(token); } }) - .on("end", x => { + .on("end", (x) => { throw new Error(`tag ${tagToken.getText()} not closed`); }); stream.start(); }, - render: function(scope, hash) { - let tokens = this.tokens.map(token => { + render: function (scope, hash) { + let tokens = this.tokens.map((token) => { return token.raw || token.getText(); }); let tokenStr = tokens.join("").trim(); - return Promise.resolve(HighlightPairedShortcode(tokenStr, this.language, this.highlightLines, options)); - } + return Promise.resolve( + HighlightPairedShortcode( + tokenStr, + this.language, + this.highlightLines, + options + ) + ); + }, }; }; diff --git a/src/markdownSyntaxHighlightOptions.js b/src/markdownSyntaxHighlightOptions.js index befb7c2..68bc983 100644 --- a/src/markdownSyntaxHighlightOptions.js +++ b/src/markdownSyntaxHighlightOptions.js @@ -1,44 +1,25 @@ -const Prism = require("prismjs"); -const PrismLoader = require("./PrismLoader"); -const HighlightLinesGroup = require("./HighlightLinesGroup"); -const getAttributes = require("./getAttributes"); +const Chroma = require("chroma-highlight"); +const parseSyntaxArguments = require("./parseSyntaxArguments"); module.exports = function (options = {}) { - return function(str, language) { - if(!language) { + return function (str, args) { + if (!args) { // empty string means defer to the upstream escaping code built into markdown lib. - return ""; - } - - - let split = language.split("/"); - if( split.length ) { - language = split.shift(); + return str; } let html; - if(language === "text") { + + if (args === "text") { html = str; } else { - html = Prism.highlight(str, PrismLoader(language), language); + const parsedArgs = parseSyntaxArguments(args, options); + + let opts = `--formatter html --html-only --html-inline-styles ${parsedArgs} `; + + html = Chroma.highlight(str, opts); } - let hasHighlightNumbers = split.length > 0; - let highlights = new HighlightLinesGroup(split.join("/"), "/"); - let lines = html.split("\n").slice(0, -1); // The last line is empty. - - lines = lines.map(function(line, j) { - if(options.alwaysWrapLineHighlights || hasHighlightNumbers) { - let lineContent = highlights.getLineMarkup(j, line); - return lineContent; - } - return line; - }); - - const context = { content: str, language: language, options: options }; - const preAttributes = getAttributes(options.preAttributes, context); - const codeAttributes = getAttributes(options.codeAttributes, context); - - return `${lines.join(options.lineSeparator || "
")}`; + return html; }; }; diff --git a/src/parseSyntaxArguments.js b/src/parseSyntaxArguments.js new file mode 100644 index 0000000..a565731 --- /dev/null +++ b/src/parseSyntaxArguments.js @@ -0,0 +1,111 @@ +// const { split } = require("liquidjs/dist/builtin/filters"); +const getAttributes = require("./getAttributes"); + +function attributeEntryToString2(attribute, context) { + let [key, value] = attribute; + + if (typeof value === "function") { + // Callback must return a string or a number + value = value(context); // Run the provided callback and store the result + } + + if (typeof value !== "string" && typeof value !== "number") { + throw new Error( + `Attribute "${key}" must have, or evaluate to, a value of type string or number, not "${typeof value}".` + ); + } + + return `${key}="${value}"`; +} + +/** + * ## Usage + * The function `getAttributes` is used to convert an object, `attributes`, with HTML attributes as keys and the values as the corresponding HTML attribute's values. + * If it is falsey, an empty string will be returned. + * + * ```js + getAttributes({ + tabindex: 0, + 'data-language': function (context) { return context.language; }, + 'data-otherStuff': 'value' + }) // => ' tabindex="0" data-language="JavaScript" data-otherStuff="value"' + ``` + * + * @param {{[s: string]: string | number}} attributes An object with key-value pairs that represent attributes. + * @param {object} context An object with the current context. + * @param {string} context.content The code to parse and highlight. + * @param {string} context.language The language for the current instance. + * @param {object} context.options The options passed to the syntax highlighter. + * @returns {string} A string containing the above HTML attributes preceded by a single space. + */ +function parseSyntaxArguments(args, context = {}) { + console.log(">>pSA"); + console.log(args); + console.log(">>>>>> context"); + console.log(context); + const preAttributes = getAttributes(context.preAttributes); + const codeAttributes = getAttributes(context.codeAttributes); + + console.log("< attributeEntryToString(entry, context) + // ); + // return formattedAttributes.length ? ` ${formattedAttributes.join(" ")}` : ""; + // } else if (typeof attributes === "string") { + // throw new Error("Syntax highlighter plugin custom attributes on
 and  must be an object. Received: " + JSON.stringify(attributes));
+  // }
+
+  return opts;
+}
+
+module.exports = parseSyntaxArguments;