work in progress. basic arguments handled in njk and md, need to do liquid and handling lineNumbersStart, highlight lines

This commit is contained in:
Robert McGovern 2023-02-02 23:46:44 +00:00
parent c40be6836a
commit 4bb213e148
10 changed files with 479 additions and 102 deletions

View File

@ -1,43 +1,53 @@
const pkg = require("./package.json"); const pkg = require("./package.json");
const Prism = require("prismjs");
const hasTemplateFormat = require("./src/hasTemplateFormat"); const hasTemplateFormat = require("./src/hasTemplateFormat");
const HighlightPairedShortcode = require("./src/HighlightPairedShortcode"); const HighlightPairedShortcode = require("./src/HighlightPairedShortcode");
const LiquidHighlightTag = require("./src/LiquidHighlightTag"); const LiquidHighlightTag = require("./src/LiquidHighlightTag");
const CharacterWrap = require("./src/CharacterWrap"); // const CharacterWrap = require("./src/CharacterWrap");
const markdownPrismJs = require("./src/markdownSyntaxHighlightOptions"); const markdownPrismJs = require("./src/markdownSyntaxHighlightOptions");
module.exports = { module.exports = {
initArguments: { Prism }, // initArguments: { Prism },
configFunction: function (eleventyConfig, options = {}) { configFunction: function (eleventyConfig, options = {}) {
try { try {
eleventyConfig.versionCheck(pkg["11ty"].compatibility); eleventyConfig.versionCheck(pkg["11ty"].compatibility);
} catch (e) { } catch (e) {
console.log( `WARN: Eleventy Plugin (${pkg.name}) Compatibility: ${e.message}` ); console.log(
`WARN: Eleventy Plugin (${pkg.name}) Compatibility: ${e.message}`
);
} }
options = Object.assign({ options = Object.assign(
{
theme: "onedark",
lineNumbers: false,
/* lineNumbersStyle: "table",*/ /* "table" or "inline" */
alwaysWrapLineHighlights: false, alwaysWrapLineHighlights: false,
// eligible to change the default to \n in a new major version. // eligible to change the default to \n in a new major version.
lineSeparator: "<br>", lineSeparator: "<br>",
preAttributes: {}, preAttributes: {},
codeAttributes: {} codeAttributes: {
}, options); theme: "onedark",
},
},
options
);
// TODO hbs?
if (hasTemplateFormat(options.templateFormats, "liquid")) { if (hasTemplateFormat(options.templateFormats, "liquid")) {
eleventyConfig.addLiquidTag("highlight", (liquidEngine) => { eleventyConfig.addLiquidTag("highlight", (liquidEngine) => {
// {% highlight js 0 2 %} // {% highlight js 0,2 %}
let highlight = new LiquidHighlightTag(liquidEngine); let highlight = new LiquidHighlightTag(liquidEngine);
return highlight.getObject(options); return highlight.getObject(options);
}); });
} }
if (hasTemplateFormat(options.templateFormats, "njk")) { if (hasTemplateFormat(options.templateFormats, "njk")) {
eleventyConfig.addPairedNunjucksShortcode("highlight", (content, args) => { eleventyConfig.addPairedNunjucksShortcode(
// {% highlight "js 0 2-3" %} "highlight",
let [language, ...highlightNumbers] = args.split(" "); (content, args) => {
return HighlightPairedShortcode(content, language, highlightNumbers.join(" "), options); // {% highlight "js 0,2-3" %}
}); return HighlightPairedShortcode(content, args, options);
}
);
} }
if (hasTemplateFormat(options.templateFormats, "md")) { if (hasTemplateFormat(options.templateFormats, "md")) {
@ -45,15 +55,24 @@ module.exports = {
eleventyConfig.addMarkdownHighlighter(markdownPrismJs(options)); eleventyConfig.addMarkdownHighlighter(markdownPrismJs(options));
} }
if( hasTemplateFormat(options.templateFormats, "11ty.js") ) { // if (hasTemplateFormat(options.templateFormats, "11ty.js")) {
eleventyConfig.addJavaScriptFunction("highlight", (language, content, highlight1, highlight2) => { // eleventyConfig.addJavaScriptFunction(
let highlightLines = [highlight1, highlight2].filter(entry => entry).join(" "); // "highlight",
let result = HighlightPairedShortcode(content, language, highlightLines, options); // (language, content, highlight1, highlight2) => {
return result; // let highlightLines = [highlight1, highlight2]
}); // .filter((entry) => entry)
} // .join(" ");
} // let result = HighlightPairedShortcode(
// content,
// args,
// options
// );
// return result;
// }
// );
// }
},
}; };
module.exports.pairedShortcode = HighlightPairedShortcode; module.exports.pairedShortcode = HighlightPairedShortcode;
module.exports.CharacterWrap = CharacterWrap; // module.exports.CharacterWrap = CharacterWrap;

View File

@ -1,8 +1,41 @@
<p align="center"><img src="https://www.11ty.dev/img/logo-github.svg" width="200" height="200" alt="eleventy Logo"></p> <p align="center"><img src="https://www.11ty.dev/img/logo-github.svg" width="200" height="200" alt="eleventy Logo"></p>
# 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.

View File

@ -8,12 +8,16 @@
<link rel="stylesheet" href="../prism-theme.css"> <link rel="stylesheet" href="../prism-theme.css">
</head> </head>
<body> <body>
Just JS
{% highlight js %} {% highlight js %}
function myFunction() { function myFunction() {
return true; return true;
} }
{% endhighlight %} {% endhighlight %}
Just JS multiline
{% highlight js %} {% highlight js %}
let multilineString = ` let multilineString = `
this is the first line this is the first line
@ -22,6 +26,7 @@ let multilineString = `
`; `;
{% endhighlight %} {% endhighlight %}
JS + Linehighlight
{% highlight js 1,3 %} {% highlight js 1,3 %}
let multilineString = ` let multilineString = `
this is the first line this is the first line
@ -29,5 +34,36 @@ let multilineString = `
this is the last line this is the last line
`; `;
{% endhighlight %} {% 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 %}
</body> </body>
</html> </html>

View File

@ -4,10 +4,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title> <title></title>
<link rel="stylesheet" href="../test.css"> <link rel="stylesheet" href="../test.css">
<link rel="stylesheet" href="../prism-theme.css"> <!-- <link rel="stylesheet" href="../prism-theme.css"> -->
</head> </head>
<body> <body>
Just `curious` what
No lang
```
function myFunction() {
return true;
}
```
Langs
```ts ```ts
function myFunction() { function myFunction() {
return true; return true;
@ -44,6 +54,7 @@ let multilineString = `
`; `;
``` ```
Highlight 1 & 3
```js/1,3 ```js/1,3
let multilineString = ` let multilineString = `
this is the first line 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 ## Scrollbar
```js ```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'; 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;
}
```
</body> </body>
</html> </html>

View File

@ -9,6 +9,8 @@
</head> </head>
<body> <body>
Just `curious` what
{% highlight "js" %} {% highlight "js" %}
function myFunction() { function myFunction() {
return true; return true;
@ -70,5 +72,28 @@ function myFunction() {
return true; return true;
} }
{% endhighlight %} {% 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 %}
</body> </body>
</html> </html>

View File

@ -13,3 +13,25 @@
.highlight-line .highlight-line:not(:last-child) { .highlight-line .highlight-line:not(:last-child) {
min-width: 0; 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;
}

View File

@ -1,34 +1,27 @@
const Prism = require("prismjs"); const Chroma = require("chroma-highlight");
const PrismLoader = require("./PrismLoader"); const parseSyntaxArguments = require("./parseSyntaxArguments");
const HighlightLinesGroup = require("./HighlightLinesGroup");
const getAttributes = require("./getAttributes"); module.exports = function (content, args, options = {}) {
// No args, so don't know language, drop out
if (!args) {
return content;
}
let highlightedContent;
module.exports = function (content, language, highlightNumbers, options = {}) {
// default to on
if (options.trim === undefined || options.trim === true) { if (options.trim === undefined || options.trim === true) {
content = content.trim(); content = content.trim();
} }
let highlightedContent; if (args === "text") {
if( language === "text" ) {
highlightedContent = content; highlightedContent = content;
} else { } else {
highlightedContent = Prism.highlight(content, PrismLoader(language), language); const parsedArgs = parseSyntaxArguments(args, options);
let opts = `--formatter html --html-only --html-inline-styles ${parsedArgs} `;
highlightedContent = Chroma.highlight(content, opts);
} }
let group = new HighlightLinesGroup(highlightNumbers); return highlightedContent;
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;
});
const context = { content: content, language: language, options: options };
const preAttributes = getAttributes(options.preAttributes, context);
const codeAttributes = getAttributes(options.codeAttributes, context);
return `<pre${preAttributes}><code${codeAttributes}>` + lines.join(options.lineSeparator || "<br>") + "</code></pre>";
}; };

View File

@ -1,4 +1,7 @@
const HighlightPairedShortcode = require("./HighlightPairedShortcode"); const HighlightPairedShortcode = require("./HighlightPairedShortcode");
const Chroma = require("chroma-highlight");
const parseSyntaxArguments = require("./parseSyntaxArguments");
const getAttributes = require("./getAttributes");
class LiquidHighlightTag { class LiquidHighlightTag {
constructor(liquidEngine) { constructor(liquidEngine) {
@ -9,6 +12,9 @@ class LiquidHighlightTag {
let ret = function (highlighter) { let ret = function (highlighter) {
return { return {
parse: function (tagToken, remainTokens) { parse: function (tagToken, remainTokens) {
console.log(">>LIQIUD");
console.log(tagToken.args);
console.log("<<LIQIUD");
let split = tagToken.args.split(" "); let split = tagToken.args.split(" ");
this.language = split.shift(); this.language = split.shift();
@ -16,29 +22,37 @@ class LiquidHighlightTag {
this.tokens = []; this.tokens = [];
var stream = highlighter.liquidEngine.parser.parseStream(remainTokens); var stream =
highlighter.liquidEngine.parser.parseStream(remainTokens);
stream stream
.on("token", token => { .on("token", (token) => {
if (token.name === "endhighlight") { if (token.name === "endhighlight") {
stream.stop(); stream.stop();
} else { } else {
this.tokens.push(token); this.tokens.push(token);
} }
}) })
.on("end", x => { .on("end", (x) => {
throw new Error(`tag ${tagToken.getText()} not closed`); throw new Error(`tag ${tagToken.getText()} not closed`);
}); });
stream.start(); stream.start();
}, },
render: function (scope, hash) { render: function (scope, hash) {
let tokens = this.tokens.map(token => { let tokens = this.tokens.map((token) => {
return token.raw || token.getText(); return token.raw || token.getText();
}); });
let tokenStr = tokens.join("").trim(); 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
)
);
},
}; };
}; };

View File

@ -1,44 +1,25 @@
const Prism = require("prismjs"); const Chroma = require("chroma-highlight");
const PrismLoader = require("./PrismLoader"); const parseSyntaxArguments = require("./parseSyntaxArguments");
const HighlightLinesGroup = require("./HighlightLinesGroup");
const getAttributes = require("./getAttributes");
module.exports = function (options = {}) { module.exports = function (options = {}) {
return function(str, language) { return function (str, args) {
if(!language) { if (!args) {
// empty string means defer to the upstream escaping code built into markdown lib. // empty string means defer to the upstream escaping code built into markdown lib.
return ""; return str;
}
let split = language.split("/");
if( split.length ) {
language = split.shift();
} }
let html; let html;
if(language === "text") {
if (args === "text") {
html = str; html = str;
} else { } 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; return html;
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 `<pre${preAttributes}><code${codeAttributes}>${lines.join(options.lineSeparator || "<br>")}</code></pre>`;
}; };
}; };

111
src/parseSyntaxArguments.js Normal file
View File

@ -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("<<pSA");
let splitArgs;
if (args.includes("/")) {
splitArgs = args.split("/");
} else if (args.includes(" ")) {
splitArgs = args.split(" ");
} else {
splitArgs = [args];
}
let opts = "";
opts += `--lexer ${splitArgs[0]} `;
if (context["theme"]) {
opts = opts + `--style ${context["theme"]} `;
} else {
opts = opts + "--style xcode-dark ";
}
if (context["lineNumbers"] || args.includes("lineNumbers")) {
opts = opts + "--html-lines ";
}
if (context["lineNumbersStyle"] == "table" || args.includes("table")) {
opts = opts + "--html-lines-table ";
}
if (splitArgs.includes("lineNumbersStart")) {
console.log("lineNumbersStart");
// console.log(splitArgs["lineNumbersStart"]);
// // console.log(args.keys());
// console.log(splitArgs.getAttribute("lineNumbersStart"));
// opts =
// opts + `--html-base-line= ${split["lineNumbersStart"].split("="[1])}`;
}
// let langClass = context.language ? `language-${context.language}` : "";
// if (!attributes) {
// return langClass ? ` class="${langClass}"` : "";
// } else if (typeof attributes === "object") {
// if(!("class" in attributes) && langClass) {
// // class attribute should be first in order
// let tempAttrs = { class: langClass };
// for(let key in attributes) {
// tempAttrs[key] = attributes[key];
// }
// attributes = tempAttrs;
// }
// const formattedAttributes = Object.entries(attributes).map(
// entry => attributeEntryToString(entry, context)
// );
// return formattedAttributes.length ? ` ${formattedAttributes.join(" ")}` : "";
// } else if (typeof attributes === "string") {
// throw new Error("Syntax highlighter plugin custom attributes on <pre> and <code> must be an object. Received: " + JSON.stringify(attributes));
// }
return opts;
}
module.exports = parseSyntaxArguments;