Skip to content

Commit

Permalink
feat: support (at least) dual themes (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
Barbapapazes authored Nov 5, 2024
1 parent af73a7f commit 45db989
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 83 deletions.
142 changes: 92 additions & 50 deletions bin/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,131 @@ const FontStyle = {
None: 0,
Italic: 1,
Bold: 2,
Underline: 4
}
Underline: 4,
};

const FONT_STYLE_TO_CSS = {
[FontStyle.Italic]: 'font-style: italic',
[FontStyle.Bold]: 'font-weight: bold',
[FontStyle.Underline]: 'text-decoration: underline'
}

const renderToHtml = function(lines, options = {}) {
const bg = options.bg || '#fff'
[FontStyle.Italic]: "font-style: italic",
[FontStyle.Bold]: "font-weight: bold",
[FontStyle.Underline]: "text-decoration: underline",
};

const renderToHtml = function (lines, options = {}) {
const theme = options.theme;
const themes = options.themes;
const highlightedLines = makeHighlightSet(options.highlightLines);
const addLines = makeHighlightSet(options.addLines);
const deleteLines = makeHighlightSet(options.deleteLines);
const focusLines = makeHighlightSet(options.focusLines);

let className = 'shiki';
let className = "shiki";

if (highlightedLines.size) {
className += ' highlighted'
className += " highlighted";
}
if (addLines.size) {
className += ' added'
className += " added";
}
if (deleteLines.size) {
className += ' deleted'
className += " deleted";
}
if (focusLines.size) {
className += ' focus'
className += " focus";
}

let html = ''
let html = "";

if (theme) {
html += `<pre class="${className}" style="background-color: ${theme.theme.bg}">`;
} else if (themes) {
const backgroundStyles = Object.entries(themes).map(
([theme, theme$]) => {
if (theme === "light") {
return `background-color:${theme$.theme.bg};`;
}

return `--shiki-${theme}-bg:${theme$.theme.bg};`;
}
);

const foregroundStyles = Object.entries(themes).map(
([theme, theme$]) => {
if (theme === "light") {
return `color:${theme$.theme.fg};`;
}

return `--shiki-${theme}:${theme$.theme.fg};`;
}
);

const classes = `${className} shiki-themes ${Object.values(themes)
.map((theme) => theme.theme.name)
.join(" ")}`;

html += `<pre class="${classes}" style="${backgroundStyles.join(
""
)}${foregroundStyles.join("")}">`;
}

html += `<pre class="${className}" style="background-color: ${bg}">`
if (options.langId) {
html += `<div class="language-id">${options.langId}</div>`
html += `<div class="language-id">${options.langId}</div>`;
}
html += `<code>`
html += `<code>`;

lines.forEach((l, index) => {
const lineNumber = index + 1;

let lineClass = 'line'
let lineClass = "line";
if (highlightedLines.has(lineNumber)) {
lineClass += ' highlight'
lineClass += " highlight";
}
if (addLines.has(lineNumber)) {
lineClass += ' add'
lineClass += " add";
}
if (deleteLines.has(lineNumber)) {
lineClass += ' del'
lineClass += " del";
}
if (focusLines.has(lineNumber)) {
lineClass += ' focus'
lineClass += " focus";
}

html += `<span class="${lineClass.trim()}">`
html += `<span class="${lineClass.trim()}">`;

l.forEach(token => {
const cssDeclarations = [`color: ${token.color || options.fg}`]
if (token.fontStyle > FontStyle.None) {
cssDeclarations.push(FONT_STYLE_TO_CSS[token.fontStyle])
l.forEach((token) => {
const cssDeclarations = [];
if (theme) {
cssDeclarations.push(`color: ${token.color || theme.theme.fg}`);
} else if (themes) {
cssDeclarations.push(token.htmlStyle);
}
html += `<span style="${cssDeclarations.join('; ')}">${escapeHtml(token.content)}</span>`
})
html += `</span>\n`
})
html = html.replace(/\n*$/, '') // Get rid of final new lines
html += `</code></pre>`

return html
}

const makeHighlightSet = function(highlightLines) {
if (token.fontStyle > FontStyle.None) {
cssDeclarations.push(FONT_STYLE_TO_CSS[token.fontStyle]);
}
html += `<span style="${cssDeclarations.join("; ")}">${escapeHtml(
token.content
)}</span>`;
});
html += `</span>\n`;
});
html = html.replace(/\n*$/, ""); // Get rid of final new lines
html += `</code></pre>`;

return html;
};

const makeHighlightSet = function (highlightLines) {
const lines = new Set();

if (! highlightLines) {
if (!highlightLines) {
return lines;
}

for (let lineSpec of highlightLines) {
if (lineSpec.toString().includes('-')) {
const [begin, end] = lineSpec.split('-').map(lineNo => Number(lineNo))
if (lineSpec.toString().includes("-")) {
const [begin, end] = lineSpec
.split("-")
.map((lineNo) => Number(lineNo));
for (let line = begin; line <= end; line++) {
lines.add(line);
}
Expand All @@ -94,19 +136,19 @@ const makeHighlightSet = function(highlightLines) {
}
}

return lines
}
return lines;
};

const htmlEscapes = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};

function escapeHtml(html) {
return html.replace(/[&<>"']/g, chr => htmlEscapes[chr])
return html.replace(/[&<>"']/g, (chr) => htmlEscapes[chr]);
}

exports.renderToHtml = renderToHtml;
109 changes: 80 additions & 29 deletions bin/shiki.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,68 @@
const fs = require('fs');
const path = require('path');
const renderer = require('./renderer');
const fs = require("fs");
const path = require("path");
const renderer = require("./renderer");
const args = JSON.parse(process.argv.slice(2));

const customLanguages = {
antlers: {
scopeName: 'text.html.statamic',
embeddedLangs: ['html'],
scopeName: "text.html.statamic",
embeddedLangs: ["html"],
},
blade: {
scopeName: 'text.html.php.blade',
embeddedLangs: ['html', 'php'],
scopeName: "text.html.php.blade",
embeddedLangs: ["html", "php"],
},
};

async function main(args) {
const shiki = await import('shiki');
const shiki = await import("shiki");
const highlighter = await shiki.getHighlighter({});

for (const [lang, spec] of Object.entries(customLanguages)) {
for (const embedded of spec.embeddedLangs) {
await highlighter.loadLanguage(embedded);
}

await highlighter.loadLanguage({ ...spec, ...loadLanguage(lang), name: lang });
await highlighter.loadLanguage({
...spec,
...loadLanguage(lang),
name: lang,
});
}

const language = args[1] || 'php';
let theme = args[2] || 'nord';
const language = args[1] || "php";

/**
* If only one theme is provided, the variable `theme` will be a string. The variable `themes` will be null.
*
* If multiple themes are provided, the variable `themes` will be an array and the variable `theme` will be null.
*/
let theme = args[2] || "nord";
let themes = null;
if (typeof args[2] === "object") {
theme = null;
themes = args[2];
}

if (fs.existsSync(theme)) {
theme = JSON.parse(fs.readFileSync(theme, 'utf-8'));
} else {
await highlighter.loadTheme(theme);
if (theme) {
if (fs.existsSync(theme)) {
theme = loadLocalTheme(theme);
} else {
await highlighter.loadTheme(theme);
}
} else if (themes) {
for (const theme of Object.values(themes)) {
if (fs.existsSync(theme)) {
themes[theme] = loadLocalTheme(theme);
} else {
await highlighter.loadTheme(theme);
}
}
}

if (!customLanguages[language]) await highlighter.loadLanguage(language);

if (args[0] === 'languages') {
if (args[0] === "languages") {
process.stdout.write(
JSON.stringify([
...Object.keys(shiki.bundledLanguagesBase),
Expand All @@ -47,7 +72,7 @@ async function main(args) {
return;
}

if (args[0] === 'aliases') {
if (args[0] === "aliases") {
process.stdout.write(
JSON.stringify([
...Object.keys(shiki.bundledLanguages),
Expand All @@ -57,33 +82,50 @@ async function main(args) {
return;
}

if (args[0] === 'themes') {
if (args[0] === "themes") {
process.stdout.write(JSON.stringify(Object.keys(shiki.bundledThemes)));
return;
}

const { theme: theme$ } = highlighter.setTheme(theme)

const result = highlighter.codeToTokens(args[0], {
theme: theme$,
const codeToTokensOptions = {
lang: language,
});
};

if (theme) {
codeToTokensOptions.theme = theme;
} else if (themes) {
codeToTokensOptions.themes = themes;
}

const result = highlighter.codeToTokens(args[0], codeToTokensOptions);

const options = args[3] || {};

const rendered = renderer.renderToHtml(result.tokens, {
fg: theme$.fg,
bg: theme$.bg,
const renderToHtmlOptions = {
highlightLines: options.highlightLines,
addLines: options.addLines,
deleteLines: options.deleteLines,
focusLines: options.focusLines,
});
};

if (theme) {
renderToHtmlOptions.theme = highlighter.setTheme(theme);
} else if (themes) {
const themes$ = {};

for (const [theme, theme$] of Object.entries(themes)) {
themes$[theme] = highlighter.setTheme(theme$);
}

renderToHtmlOptions.themes = themes$;
}

const rendered = renderer.renderToHtml(result.tokens, renderToHtmlOptions);

process.stdout.write(rendered);
}

main(args)
main(args);

function loadLanguage(language) {
const path = getLanguagePath(language);
Expand All @@ -93,7 +135,16 @@ function loadLanguage(language) {
}

function getLanguagePath(language) {
const url = path.join(__dirname, '..', 'languages', `${language}.tmLanguage.json`);
const url = path.join(
__dirname,
"..",
"languages",
`${language}.tmLanguage.json`
);

return path.normalize(url);
}

function loadLocalTheme(theme) {
return JSON.parse(fs.readFileSync(theme, "utf-8"));
}
7 changes: 5 additions & 2 deletions src/Shiki.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ public static function setCustomWorkingDirPath(?string $path)
static::$customWorkingDirPath = $path;
}

/**
* @param string|array<string, string>|null $theme Can be a single theme or an array with a light and a dark theme.
*/
public static function highlight(
string $code,
?string $language = null,
?string $theme = null,
mixed $theme = null,
?array $highlightLines = null,
?array $addLines = null,
?array $deleteLines = null,
Expand Down Expand Up @@ -74,7 +77,7 @@ public function themeIsAvailable(string $theme): bool
return in_array($theme, $this->getAvailableThemes());
}

public function highlightCode(string $code, string $language, ?string $theme = null, ?array $options = []): string
public function highlightCode(string $code, string $language, mixed $theme = null, ?array $options = []): string
{
$theme = $theme ?? $this->defaultTheme;

Expand Down
Loading

0 comments on commit 45db989

Please sign in to comment.