gatsby-remark-shiki-twoslash
Advanced tools
Comparing version 0.6.1 to 0.7.0
@@ -7,320 +7,7 @@ 'use strict'; | ||
var shiki = require('shiki'); | ||
var shikiLanguages = require('shiki-languages'); | ||
var twoslash = require('@typescript/twoslash'); | ||
var vfs = require('@typescript/vfs'); | ||
var shikiTwoslash = require('shiki-twoslash'); | ||
var visit = _interopDefault(require('unist-util-visit')); | ||
var splice = function splice(str, idx, rem, newString) { | ||
return str.slice(0, idx) + newString + str.slice(idx + Math.abs(rem)); | ||
}; | ||
// prettier-ignore | ||
/** | ||
* We're given the text which lives inside the token, and this function will | ||
* annotate it with twoslash metadata | ||
*/ | ||
function createHighlightedString2(ranges, text) { | ||
var actions = []; | ||
var hasErrors = false; // Why the weird chars? We need to make sure that generic syntax isn't | ||
// interpreted as html tags - to do that we need to switch out < to < - *but* | ||
// making that transition changes the indexes because it's gone from 1 char to 4 chars | ||
// So, use an obscure character to indicate a real < for HTML, then switch it after | ||
ranges.forEach(function (r) { | ||
if (r.classes === "lsp") { | ||
actions.push({ | ||
text: "⇍/data-lsp⇏", | ||
index: r.end | ||
}); | ||
actions.push({ | ||
text: "\u21CDdata-lsp lsp=\u21EF" + (r.lsp || "") + "\u21EF\u21CF", | ||
index: r.begin | ||
}); | ||
} else if (r.classes === "err") { | ||
hasErrors = true; | ||
} else if (r.classes === "query") { | ||
actions.push({ | ||
text: "⇍/data-highlight⇏", | ||
index: r.end | ||
}); | ||
actions.push({ | ||
text: "\u21CDdata-highlight'\u21CF", | ||
index: r.begin | ||
}); | ||
} | ||
}); | ||
var html = (" " + text).slice(1); // Apply all the edits | ||
actions.sort(function (l, r) { | ||
return r.index - l.index; | ||
}).forEach(function (action) { | ||
html = splice(html, action.index, 0, action.text); | ||
}); | ||
if (hasErrors) html = "\u21CDdata-err\u21CF" + html + "\u21CD/data-err\u21CF"; | ||
return replaceTripleArrow(stripHTML(html)); | ||
} | ||
var subTripleArrow = function subTripleArrow(str) { | ||
return str.replace(/</g, "⇍").replace(/>/g, "⇏").replace(/'/g, "⇯"); | ||
}; | ||
var replaceTripleArrow = function replaceTripleArrow(str) { | ||
return str.replace(/⇍/g, "<").replace(/⇏/g, ">").replace(/⇯/g, "'"); | ||
}; | ||
var replaceTripleArrowEncoded = function replaceTripleArrowEncoded(str) { | ||
return str.replace(/⇍/g, "<").replace(/⇏/g, ">").replace(/⇯/g, "'"); | ||
}; | ||
function stripHTML(text) { | ||
var table = { | ||
"<": "lt", | ||
'"': "quot", | ||
"'": "apos", | ||
"&": "amp", | ||
"\r": "#10", | ||
"\n": "#13" | ||
}; | ||
return text.toString().replace(/[<"'\r\n&]/g, function (chr) { | ||
return "&" + table[chr] + ";"; | ||
}); | ||
} | ||
// This started as a JS port of https://github.com/octref/shiki/blob/master/packages/shiki/src/renderer.ts | ||
// What we're trying to do is merge two sets of information into a single tree for HTML | ||
// 1: Syntax highlight info from shiki | ||
// 2: Twoslash metadata like errors, indentifiers etc | ||
// Because shiki gives use a set of lines to work from, then the first thing which happens | ||
// is converting twoslash data into the same format. | ||
// Things which make it hard: | ||
// | ||
// - Twoslash results can be cut, so sometimes there is edge cases between twoslash results | ||
// - Twoslash results can be multi-file | ||
// - the DOM requires a flattened graph of html elements | ||
// | ||
function renderToHTML(lines, options, twoslash) { | ||
// It's a NOOP for us with twoslash, this is basically all | ||
// the other languages | ||
if (!twoslash) { | ||
return plainOleShikiRenderer(lines, options); | ||
} | ||
var html = ""; | ||
html += "<pre class=\"shiki twoslash\">"; | ||
if (options.langId) { | ||
html += "<div class=\"language-id\">" + options.langId + "</div>"; | ||
} | ||
html += "<div class='code-container'><code>"; | ||
var errorsGroupedByLine = groupBy(twoslash.errors, function (e) { | ||
return e.line; | ||
}) || new Map(); | ||
var staticQuickInfosGroupedByLine = groupBy(twoslash.staticQuickInfos, function (q) { | ||
return q.line; | ||
}) || new Map(); // A query is always about the line above it! | ||
var queriesGroupedByLine = groupBy(twoslash.queries, function (q) { | ||
return q.line - 1; | ||
}) || new Map(); | ||
var filePos = 0; | ||
lines.forEach(function (l, i) { | ||
var errors = errorsGroupedByLine.get(i) || []; | ||
var lspValues = staticQuickInfosGroupedByLine.get(i) || []; | ||
var queries = queriesGroupedByLine.get(i) || []; | ||
if (l.length === 0 && i === 0) { | ||
// Skip the first newline if it's blank | ||
filePos += 1; | ||
} else if (l.length === 0) { | ||
filePos += 1; | ||
html += "\n"; | ||
} else { | ||
// Keep track of the position of the current token in a line so we can match it up to the | ||
// errors and lang serv identifiers | ||
var tokenPos = 0; | ||
l.forEach(function (token) { | ||
var tokenContent = ""; // Underlining particular words | ||
var findTokenFunc = function findTokenFunc(start) { | ||
return function (e) { | ||
return start <= e.character && start + token.content.length >= e.character + e.length; | ||
}; | ||
}; | ||
var errorsInToken = errors.filter(findTokenFunc(tokenPos)); | ||
var lspResponsesInToken = lspValues.filter(findTokenFunc(tokenPos)); | ||
var queriesInToken = queries.filter(findTokenFunc(tokenPos)); | ||
var allTokens = [].concat(errorsInToken, lspResponsesInToken, queriesInToken); | ||
var allTokensByStart = allTokens.sort(function (l, r) { | ||
return (l.start || 0) - (r.start || 0); | ||
}); | ||
if (allTokensByStart.length) { | ||
var ranges = allTokensByStart.map(function (token) { | ||
var range = { | ||
begin: token.start - filePos, | ||
end: token.start + token.length - filePos | ||
}; | ||
if ("renderedMessage" in token) range.classes = "err"; | ||
if ("kind" in token) range.classes = token.kind; | ||
if ("targetString" in token) { | ||
range.classes = "lsp"; | ||
range["lsp"] = stripHTML(token.text); | ||
} | ||
return range; | ||
}); | ||
tokenContent += createHighlightedString2(ranges, token.content); | ||
} else { | ||
tokenContent += subTripleArrow(token.content); | ||
} | ||
html += "<span style=\"color: " + token.color + "\">" + tokenContent + "</span>"; | ||
tokenPos += token.content.length; | ||
filePos += token.content.length; | ||
}); | ||
html += "\n"; | ||
filePos += 1; | ||
} // Adding error messages to the line after | ||
if (errors.length) { | ||
var messages = errors.map(function (e) { | ||
return escapeHtml(e.renderedMessage); | ||
}).join("</br>"); | ||
var codes = errors.map(function (e) { | ||
return e.code; | ||
}).join("<br/>"); | ||
html += "<span class=\"error\"><span>" + messages + "</span><span class=\"code\">" + codes + "</span></span>"; | ||
html += "<span class=\"error-behind\">" + messages + "</span>"; | ||
} // Add queries to the next line | ||
if (queries.length) { | ||
queries.forEach(function (query) { | ||
switch (query.kind) { | ||
case "query": | ||
{ | ||
html += "<span class='query'>" + ("//" + "".padStart(query.offset - 2) + "^ = " + query.text) + "</span>"; | ||
break; | ||
} | ||
case "completions": | ||
{ | ||
if (!query.completions) { | ||
html += "<span class='query'>" + ("//" + "".padStart(query.offset - 2) + "^ - No completions found") + "</span>"; | ||
} else { | ||
var prefixed = query.completions.filter(function (c) { | ||
return c.name.startsWith(query.completionsPrefix || "____"); | ||
}); | ||
console.log("Prefix: ", query.completionsPrefix); | ||
var lis = prefixed.sort(function (l, r) { | ||
return l.name.localeCompare(r.name); | ||
}).map(function (c) { | ||
var _query$completionsPre; | ||
var after = c.name.substr(((_query$completionsPre = query.completionsPrefix) === null || _query$completionsPre === void 0 ? void 0 : _query$completionsPre.length) || 0); | ||
var name = "<span><span class='result-found'>" + (query.completionsPrefix || "") + "</span>" + after + "<span>"; | ||
return "<li>" + name + "</li>"; | ||
}).join(""); | ||
html += "".padStart(query.offset) + ("<span class='inline-completions'><ul class='dropdown'>" + lis + "</ul></span>"); | ||
} | ||
} | ||
} | ||
}); | ||
html += "\n"; | ||
} | ||
}); | ||
html = replaceTripleArrowEncoded(html.replace(/\n*$/, "")); // Get rid of final new lines | ||
var playgroundLink = "<a href='" + twoslash.playgroundURL + "'>Try</a>"; | ||
html += "</code>" + playgroundLink + "</div></pre>"; | ||
return html; | ||
} | ||
function escapeHtml(html) { | ||
return html.replace(/</g, "<").replace(/>/g, ">"); | ||
} | ||
/** Returns a map where all the keys are the value in keyGetter */ | ||
function groupBy(list, keyGetter) { | ||
var map = new Map(); | ||
list.forEach(function (item) { | ||
var key = keyGetter(item); | ||
var collection = map.get(key); | ||
if (!collection) { | ||
map.set(key, [item]); | ||
} else { | ||
collection.push(item); | ||
} | ||
}); | ||
return map; | ||
} | ||
function plainOleShikiRenderer(lines, options) { | ||
var html = ""; | ||
html += "<pre class=\"shiki\">"; | ||
if (options.langId) { | ||
html += "<div class=\"language-id\">" + options.langId + "</div>"; | ||
} | ||
html += "<div class='code-container'><code>"; | ||
lines.forEach(function (l) { | ||
if (l.length === 0) { | ||
html += "\n"; | ||
} else { | ||
l.forEach(function (token) { | ||
html += "<span style=\"color: " + token.color + "\">" + escapeHtml(token.content) + "</span>"; | ||
}); | ||
html += "\n"; | ||
} | ||
}); | ||
html = html.replace(/\n*$/, ""); // Get rid of final new lines | ||
html += "</code></div></pre>"; | ||
return html; | ||
} | ||
var languages = | ||
/*#__PURE__*/ | ||
[].concat(shikiLanguages.commonLangIds, shikiLanguages.commonLangAliases, shikiLanguages.otherLangIds); | ||
/** | ||
* This gets filled in by the promise below, then should | ||
* hopefully be more or less synchronous access by each parse | ||
* of the highlighter | ||
*/ | ||
var highlighter = null; | ||
var getHighlighterObj = function getHighlighterObj(options) { | ||
if (highlighter) return highlighter; | ||
var settings = options || {}; | ||
var theme = settings.theme || "nord"; | ||
var shikiTheme; | ||
try { | ||
shikiTheme = shiki.getTheme(theme); | ||
} catch (error) { | ||
try { | ||
shikiTheme = shiki.loadTheme(theme); | ||
} catch (error) { | ||
throw new Error("Unable to load theme: " + theme + " - " + error.message); | ||
} | ||
} | ||
return shiki.getHighlighter({ | ||
theme: shikiTheme, | ||
langs: languages | ||
}).then(function (newHighlighter) { | ||
highlighter = newHighlighter; | ||
return highlighter; | ||
}); | ||
}; | ||
var defaultSettings = {}; | ||
/** | ||
* The function doing the work of transforming any codeblock samples | ||
@@ -330,8 +17,9 @@ * which have opted-in to the twoslash pattern. | ||
var visitor = function visitor(twoslashSettings) { | ||
var visitor = function visitor(highlighter, twoslashSettings) { | ||
return function (node) { | ||
var lang = node.lang; | ||
var settings = twoslashSettings || defaultSettings; // Run twoslash | ||
var settings = twoslashSettings || {}; | ||
var shouldDisableTwoslash = process && process.env && !!process.env.TWOSLASH_DISABLE; // Run twoslash | ||
runTwoSlashOnNode(settings)(node); // Shiki doesn't respect json5 as an input, so switch it | ||
if (!shouldDisableTwoslash) runTwoSlashOnNode(settings)(node); // Shiki doesn't respect json5 as an input, so switch it | ||
// to json, which can handle comments in the syntax highlight | ||
@@ -344,14 +32,20 @@ | ||
if (replacer[lang]) lang = replacer[lang]; | ||
var shouldDisableTwoslash = process && process.env && !!process.env.TWOSLASH_DISABLE; // Check we can highlight and render | ||
var results = shikiTwoslash.renderCodeToHTML(node.value, lang, node.meta || [], {}, highlighter, node.twoslash); | ||
node.type = "html"; | ||
node.value = results; | ||
node.children = []; | ||
}; | ||
}; | ||
/** | ||
* Runs twoslash across an AST node, switching out the text content, and lang | ||
* and adding a `twoslash` property to the node. | ||
*/ | ||
var shouldHighlight = lang && languages.includes(lang); | ||
if (shouldHighlight && !shouldDisableTwoslash) { | ||
var tokens = highlighter.codeToThemedTokens(node.value, lang); | ||
var results = renderToHTML(tokens, { | ||
langId: lang | ||
}, node.twoslash); | ||
node.type = "html"; | ||
node.value = results; | ||
node.children = []; | ||
var runTwoSlashOnNode = function runTwoSlashOnNode(settings) { | ||
return function (node) { | ||
if (node.meta && node.meta.includes("twoslash")) { | ||
var results = shikiTwoslash.runTwoSlash(node.value, node.lang, settings); | ||
node.value = results.code; | ||
node.lang = results.extension; | ||
node.twoslash = results; | ||
} | ||
@@ -366,3 +60,2 @@ }; | ||
var remarkShiki = function remarkShiki(_ref, shikiSettings, settings) { | ||
@@ -372,4 +65,4 @@ var markdownAST = _ref.markdownAST; | ||
try { | ||
return Promise.resolve(getHighlighterObj(shikiSettings)).then(function () { | ||
visit(markdownAST, "code", visitor(settings)); | ||
return Promise.resolve(shikiTwoslash.createShikiHighlighter(shikiSettings)).then(function (highlighter) { | ||
visit(markdownAST, "code", visitor(highlighter, settings)); | ||
}); | ||
@@ -379,36 +72,9 @@ } catch (e) { | ||
} | ||
}; /////////////////// Mainly for internal use, but tests could use this, not considered public API, so could change | ||
/** @internal */ | ||
var runTwoSlashOnNode = function runTwoSlashOnNode(settings) { | ||
return function (node) { | ||
// Run twoslash and replace the main contents if | ||
// the ``` has 'twoslash' after it | ||
if (node.meta && node.meta.includes("twoslash")) { | ||
var map = undefined; | ||
if (settings.useNodeModules) { | ||
var laterESVersion = 6; // we don't want a hard dep on TS, so that browsers can run this code) | ||
map = vfs.createDefaultMapFromNodeModules({ | ||
target: laterESVersion | ||
}); // Add @types to the fsmap | ||
vfs.addAllFilesFromFolder(map, settings.nodeModulesTypesPath || "node_modules/@types"); | ||
} | ||
var results = twoslash.twoslasher(node.value, node.lang, undefined, undefined, undefined, map); | ||
node.value = results.code; | ||
node.lang = results.extension; | ||
node.twoslash = results; | ||
} | ||
}; | ||
}; | ||
/** Sends the twoslash visitor over the existing MD AST and replaces the code samples inline, does not do highlighting */ | ||
var runTwoSlashAcrossDocument = function runTwoSlashAcrossDocument(_ref2, settings) { | ||
var markdownAST = _ref2.markdownAST; | ||
return visit(markdownAST, "code", runTwoSlashOnNode(settings || defaultSettings)); | ||
return visit(markdownAST, "code", runTwoSlashOnNode(settings || {})); | ||
}; | ||
@@ -419,2 +85,3 @@ | ||
exports.runTwoSlashOnNode = runTwoSlashOnNode; | ||
exports.visitor = visitor; | ||
//# sourceMappingURL=gatsby-remark-shiki-twoslash.cjs.development.js.map |
@@ -1,2 +0,2 @@ | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,n=require("shiki"),t=require("shiki-languages"),r=require("@typescript/twoslash"),a=require("@typescript/vfs"),s=(e=require("unist-util-visit"))&&"object"==typeof e&&"default"in e?e.default:e;function o(e){var n={"<":"lt",'"':"quot","'":"apos","&":"amp","\r":"#10","\n":"#13"};return e.toString().replace(/[<"'\r\n&]/g,(function(e){return"&"+n[e]+";"}))}function i(e){return e.replace(/</g,"<").replace(/>/g,">")}function c(e,n){var t=new Map;return e.forEach((function(e){var r=n(e),a=t.get(r);a?a.push(e):t.set(r,[e])})),t}var l=[].concat(t.commonLangIds,t.commonLangAliases,t.otherLangIds),u=null,p={},d=function(e){return function(n){if(n.meta&&n.meta.includes("twoslash")){var t=void 0;e.useNodeModules&&(t=a.createDefaultMapFromNodeModules({target:6}),a.addAllFilesFromFolder(t,e.nodeModulesTypesPath||"node_modules/@types"));var s=r.twoslasher(n.value,n.lang,void 0,void 0,void 0,t);n.value=s.code,n.lang=s.extension,n.twoslash=s}}};exports.default=function(e,t,r){var a=e.markdownAST;try{return Promise.resolve(function(e){if(u)return u;var t,r=(e||{}).theme||"nord";try{t=n.getTheme(r)}catch(e){try{t=n.loadTheme(r)}catch(e){throw new Error("Unable to load theme: "+r+" - "+e.message)}}return n.getHighlighter({theme:t,langs:l}).then((function(e){return u=e}))}(t)).then((function(){var e;s(a,"code",(e=r,function(n){var t=n.lang;d(e||p)(n);var r={json5:"json"};r[t]&&(t=r[t]);var a=process&&process.env&&!!process.env.TWOSLASH_DISABLE;if(t&&l.includes(t)&&!a){var s=function(e,n,t){if(!t)return function(e,n){var t="";return t+='<pre class="shiki">',n.langId&&(t+='<div class="language-id">'+n.langId+"</div>"),t+="<div class='code-container'><code>",e.forEach((function(e){0===e.length?t+="\n":(e.forEach((function(e){t+='<span style="color: '+e.color+'">'+i(e.content)+"</span>"})),t+="\n")})),t=t.replace(/\n*$/,""),t+="</code></div></pre>"}(e,n);var r="";r+='<pre class="shiki twoslash">',n.langId&&(r+='<div class="language-id">'+n.langId+"</div>"),r+="<div class='code-container'><code>";var a,s=c(t.errors,(function(e){return e.line}))||new Map,l=c(t.staticQuickInfos,(function(e){return e.line}))||new Map,u=c(t.queries,(function(e){return e.line-1}))||new Map,p=0;return e.forEach((function(e,n){var t=s.get(n)||[],a=l.get(n)||[],c=u.get(n)||[];if(0===e.length&&0===n)p+=1;else if(0===e.length)p+=1,r+="\n";else{var d=0;e.forEach((function(e){var n="",s=function(n){return function(t){return n<=t.character&&n+e.content.length>=t.character+t.length}},i=t.filter(s(d)),l=a.filter(s(d)),u=c.filter(s(d)),f=[].concat(i,l,u).sort((function(e,n){return(e.start||0)-(n.start||0)}));n+=f.length?function(e,n){var t=[],r=!1;e.forEach((function(e){"lsp"===e.classes?(t.push({text:"⇍/data-lsp⇏",index:e.end}),t.push({text:"⇍data-lsp lsp=⇯"+(e.lsp||"")+"⇯⇏",index:e.begin})):"err"===e.classes?r=!0:"query"===e.classes&&(t.push({text:"⇍/data-highlight⇏",index:e.end}),t.push({text:"⇍data-highlight'⇏",index:e.begin}))}));var a=(" "+n).slice(1);return t.sort((function(e,n){return n.index-e.index})).forEach((function(e){var n,t,r;r=e.text,a=(n=a).slice(0,t=e.index)+r+n.slice(t+Math.abs(0))})),r&&(a="⇍data-err⇏"+a+"⇍/data-err⇏"),o(a).replace(/⇍/g,"<").replace(/⇏/g,">").replace(/⇯/g,"'")}(f.map((function(e){var n={begin:e.start-p,end:e.start+e.length-p};return"renderedMessage"in e&&(n.classes="err"),"kind"in e&&(n.classes=e.kind),"targetString"in e&&(n.classes="lsp",n.lsp=o(e.text)),n})),e.content):e.content.replace(/</g,"⇍").replace(/>/g,"⇏").replace(/'/g,"⇯"),r+='<span style="color: '+e.color+'">'+n+"</span>",d+=e.content.length,p+=e.content.length})),r+="\n",p+=1}if(t.length){var f=t.map((function(e){return i(e.renderedMessage)})).join("</br>"),g=t.map((function(e){return e.code})).join("<br/>");r+='<span class="error"><span>'+f+'</span><span class="code">'+g+"</span></span>",r+='<span class="error-behind">'+f+"</span>"}c.length&&(c.forEach((function(e){switch(e.kind){case"query":r+="<span class='query'>//"+"".padStart(e.offset-2)+"^ = "+e.text+"</span>";break;case"completions":if(e.completions){var n=e.completions.filter((function(n){return n.name.startsWith(e.completionsPrefix||"____")}));console.log("Prefix: ",e.completionsPrefix);var t=n.sort((function(e,n){return e.name.localeCompare(n.name)})).map((function(n){var t,r=n.name.substr((null===(t=e.completionsPrefix)||void 0===t?void 0:t.length)||0);return"<li><span><span class='result-found'>"+(e.completionsPrefix||"")+"</span>"+r+"<span></li>"})).join("");r+="".padStart(e.offset)+"<span class='inline-completions'><ul class='dropdown'>"+t+"</ul></span>"}else r+="<span class='query'>//"+"".padStart(e.offset-2)+"^ - No completions found</span>"}})),r+="\n")})),a=r.replace(/\n*$/,""),r=a.replace(/⇍/g,"<").replace(/⇏/g,">").replace(/⇯/g,"'"),r+="</code><a href='"+t.playgroundURL+"'>Try</a></div></pre>"}(u.codeToThemedTokens(n.value,t),{langId:t},n.twoslash);n.type="html",n.value=s,n.children=[]}}))}))}catch(e){return Promise.reject(e)}},exports.runTwoSlashAcrossDocument=function(e,n){return s(e.markdownAST,"code",d(n||p))},exports.runTwoSlashOnNode=d; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,r=require("shiki-twoslash"),t=(e=require("unist-util-visit"))&&"object"==typeof e&&"default"in e?e.default:e,o=function(e,t){return function(o){var s=o.lang;process&&process.env&&process.env.TWOSLASH_DISABLE||n(t||{})(o);var a={json5:"json"};a[s]&&(s=a[s]);var u=r.renderCodeToHTML(o.value,s,o.meta||[],{},e,o.twoslash);o.type="html",o.value=u,o.children=[]}},n=function(e){return function(t){if(t.meta&&t.meta.includes("twoslash")){var o=r.runTwoSlash(t.value,t.lang,e);t.value=o.code,t.lang=o.extension,t.twoslash=o}}};exports.default=function(e,n,s){var a=e.markdownAST;try{return Promise.resolve(r.createShikiHighlighter(n)).then((function(e){t(a,"code",o(e,s))}))}catch(e){return Promise.reject(e)}},exports.runTwoSlashAcrossDocument=function(e,r){return t(e.markdownAST,"code",n(r||{}))},exports.runTwoSlashOnNode=n,exports.visitor=o; | ||
//# sourceMappingURL=gatsby-remark-shiki-twoslash.cjs.production.min.js.map |
@@ -1,319 +0,6 @@ | ||
import { getTheme, loadTheme, getHighlighter } from 'shiki'; | ||
import { commonLangIds, commonLangAliases, otherLangIds } from 'shiki-languages'; | ||
import { twoslasher } from '@typescript/twoslash'; | ||
import { createDefaultMapFromNodeModules, addAllFilesFromFolder } from '@typescript/vfs'; | ||
import { runTwoSlash, renderCodeToHTML, createShikiHighlighter } from 'shiki-twoslash'; | ||
import visit from 'unist-util-visit'; | ||
var splice = function splice(str, idx, rem, newString) { | ||
return str.slice(0, idx) + newString + str.slice(idx + Math.abs(rem)); | ||
}; | ||
// prettier-ignore | ||
/** | ||
* We're given the text which lives inside the token, and this function will | ||
* annotate it with twoslash metadata | ||
*/ | ||
function createHighlightedString2(ranges, text) { | ||
var actions = []; | ||
var hasErrors = false; // Why the weird chars? We need to make sure that generic syntax isn't | ||
// interpreted as html tags - to do that we need to switch out < to < - *but* | ||
// making that transition changes the indexes because it's gone from 1 char to 4 chars | ||
// So, use an obscure character to indicate a real < for HTML, then switch it after | ||
ranges.forEach(function (r) { | ||
if (r.classes === "lsp") { | ||
actions.push({ | ||
text: "⇍/data-lsp⇏", | ||
index: r.end | ||
}); | ||
actions.push({ | ||
text: "\u21CDdata-lsp lsp=\u21EF" + (r.lsp || "") + "\u21EF\u21CF", | ||
index: r.begin | ||
}); | ||
} else if (r.classes === "err") { | ||
hasErrors = true; | ||
} else if (r.classes === "query") { | ||
actions.push({ | ||
text: "⇍/data-highlight⇏", | ||
index: r.end | ||
}); | ||
actions.push({ | ||
text: "\u21CDdata-highlight'\u21CF", | ||
index: r.begin | ||
}); | ||
} | ||
}); | ||
var html = (" " + text).slice(1); // Apply all the edits | ||
actions.sort(function (l, r) { | ||
return r.index - l.index; | ||
}).forEach(function (action) { | ||
html = splice(html, action.index, 0, action.text); | ||
}); | ||
if (hasErrors) html = "\u21CDdata-err\u21CF" + html + "\u21CD/data-err\u21CF"; | ||
return replaceTripleArrow(stripHTML(html)); | ||
} | ||
var subTripleArrow = function subTripleArrow(str) { | ||
return str.replace(/</g, "⇍").replace(/>/g, "⇏").replace(/'/g, "⇯"); | ||
}; | ||
var replaceTripleArrow = function replaceTripleArrow(str) { | ||
return str.replace(/⇍/g, "<").replace(/⇏/g, ">").replace(/⇯/g, "'"); | ||
}; | ||
var replaceTripleArrowEncoded = function replaceTripleArrowEncoded(str) { | ||
return str.replace(/⇍/g, "<").replace(/⇏/g, ">").replace(/⇯/g, "'"); | ||
}; | ||
function stripHTML(text) { | ||
var table = { | ||
"<": "lt", | ||
'"': "quot", | ||
"'": "apos", | ||
"&": "amp", | ||
"\r": "#10", | ||
"\n": "#13" | ||
}; | ||
return text.toString().replace(/[<"'\r\n&]/g, function (chr) { | ||
return "&" + table[chr] + ";"; | ||
}); | ||
} | ||
// This started as a JS port of https://github.com/octref/shiki/blob/master/packages/shiki/src/renderer.ts | ||
// What we're trying to do is merge two sets of information into a single tree for HTML | ||
// 1: Syntax highlight info from shiki | ||
// 2: Twoslash metadata like errors, indentifiers etc | ||
// Because shiki gives use a set of lines to work from, then the first thing which happens | ||
// is converting twoslash data into the same format. | ||
// Things which make it hard: | ||
// | ||
// - Twoslash results can be cut, so sometimes there is edge cases between twoslash results | ||
// - Twoslash results can be multi-file | ||
// - the DOM requires a flattened graph of html elements | ||
// | ||
function renderToHTML(lines, options, twoslash) { | ||
// It's a NOOP for us with twoslash, this is basically all | ||
// the other languages | ||
if (!twoslash) { | ||
return plainOleShikiRenderer(lines, options); | ||
} | ||
var html = ""; | ||
html += "<pre class=\"shiki twoslash\">"; | ||
if (options.langId) { | ||
html += "<div class=\"language-id\">" + options.langId + "</div>"; | ||
} | ||
html += "<div class='code-container'><code>"; | ||
var errorsGroupedByLine = groupBy(twoslash.errors, function (e) { | ||
return e.line; | ||
}) || new Map(); | ||
var staticQuickInfosGroupedByLine = groupBy(twoslash.staticQuickInfos, function (q) { | ||
return q.line; | ||
}) || new Map(); // A query is always about the line above it! | ||
var queriesGroupedByLine = groupBy(twoslash.queries, function (q) { | ||
return q.line - 1; | ||
}) || new Map(); | ||
var filePos = 0; | ||
lines.forEach(function (l, i) { | ||
var errors = errorsGroupedByLine.get(i) || []; | ||
var lspValues = staticQuickInfosGroupedByLine.get(i) || []; | ||
var queries = queriesGroupedByLine.get(i) || []; | ||
if (l.length === 0 && i === 0) { | ||
// Skip the first newline if it's blank | ||
filePos += 1; | ||
} else if (l.length === 0) { | ||
filePos += 1; | ||
html += "\n"; | ||
} else { | ||
// Keep track of the position of the current token in a line so we can match it up to the | ||
// errors and lang serv identifiers | ||
var tokenPos = 0; | ||
l.forEach(function (token) { | ||
var tokenContent = ""; // Underlining particular words | ||
var findTokenFunc = function findTokenFunc(start) { | ||
return function (e) { | ||
return start <= e.character && start + token.content.length >= e.character + e.length; | ||
}; | ||
}; | ||
var errorsInToken = errors.filter(findTokenFunc(tokenPos)); | ||
var lspResponsesInToken = lspValues.filter(findTokenFunc(tokenPos)); | ||
var queriesInToken = queries.filter(findTokenFunc(tokenPos)); | ||
var allTokens = [].concat(errorsInToken, lspResponsesInToken, queriesInToken); | ||
var allTokensByStart = allTokens.sort(function (l, r) { | ||
return (l.start || 0) - (r.start || 0); | ||
}); | ||
if (allTokensByStart.length) { | ||
var ranges = allTokensByStart.map(function (token) { | ||
var range = { | ||
begin: token.start - filePos, | ||
end: token.start + token.length - filePos | ||
}; | ||
if ("renderedMessage" in token) range.classes = "err"; | ||
if ("kind" in token) range.classes = token.kind; | ||
if ("targetString" in token) { | ||
range.classes = "lsp"; | ||
range["lsp"] = stripHTML(token.text); | ||
} | ||
return range; | ||
}); | ||
tokenContent += createHighlightedString2(ranges, token.content); | ||
} else { | ||
tokenContent += subTripleArrow(token.content); | ||
} | ||
html += "<span style=\"color: " + token.color + "\">" + tokenContent + "</span>"; | ||
tokenPos += token.content.length; | ||
filePos += token.content.length; | ||
}); | ||
html += "\n"; | ||
filePos += 1; | ||
} // Adding error messages to the line after | ||
if (errors.length) { | ||
var messages = errors.map(function (e) { | ||
return escapeHtml(e.renderedMessage); | ||
}).join("</br>"); | ||
var codes = errors.map(function (e) { | ||
return e.code; | ||
}).join("<br/>"); | ||
html += "<span class=\"error\"><span>" + messages + "</span><span class=\"code\">" + codes + "</span></span>"; | ||
html += "<span class=\"error-behind\">" + messages + "</span>"; | ||
} // Add queries to the next line | ||
if (queries.length) { | ||
queries.forEach(function (query) { | ||
switch (query.kind) { | ||
case "query": | ||
{ | ||
html += "<span class='query'>" + ("//" + "".padStart(query.offset - 2) + "^ = " + query.text) + "</span>"; | ||
break; | ||
} | ||
case "completions": | ||
{ | ||
if (!query.completions) { | ||
html += "<span class='query'>" + ("//" + "".padStart(query.offset - 2) + "^ - No completions found") + "</span>"; | ||
} else { | ||
var prefixed = query.completions.filter(function (c) { | ||
return c.name.startsWith(query.completionsPrefix || "____"); | ||
}); | ||
console.log("Prefix: ", query.completionsPrefix); | ||
var lis = prefixed.sort(function (l, r) { | ||
return l.name.localeCompare(r.name); | ||
}).map(function (c) { | ||
var _query$completionsPre; | ||
var after = c.name.substr(((_query$completionsPre = query.completionsPrefix) === null || _query$completionsPre === void 0 ? void 0 : _query$completionsPre.length) || 0); | ||
var name = "<span><span class='result-found'>" + (query.completionsPrefix || "") + "</span>" + after + "<span>"; | ||
return "<li>" + name + "</li>"; | ||
}).join(""); | ||
html += "".padStart(query.offset) + ("<span class='inline-completions'><ul class='dropdown'>" + lis + "</ul></span>"); | ||
} | ||
} | ||
} | ||
}); | ||
html += "\n"; | ||
} | ||
}); | ||
html = replaceTripleArrowEncoded(html.replace(/\n*$/, "")); // Get rid of final new lines | ||
var playgroundLink = "<a href='" + twoslash.playgroundURL + "'>Try</a>"; | ||
html += "</code>" + playgroundLink + "</div></pre>"; | ||
return html; | ||
} | ||
function escapeHtml(html) { | ||
return html.replace(/</g, "<").replace(/>/g, ">"); | ||
} | ||
/** Returns a map where all the keys are the value in keyGetter */ | ||
function groupBy(list, keyGetter) { | ||
var map = new Map(); | ||
list.forEach(function (item) { | ||
var key = keyGetter(item); | ||
var collection = map.get(key); | ||
if (!collection) { | ||
map.set(key, [item]); | ||
} else { | ||
collection.push(item); | ||
} | ||
}); | ||
return map; | ||
} | ||
function plainOleShikiRenderer(lines, options) { | ||
var html = ""; | ||
html += "<pre class=\"shiki\">"; | ||
if (options.langId) { | ||
html += "<div class=\"language-id\">" + options.langId + "</div>"; | ||
} | ||
html += "<div class='code-container'><code>"; | ||
lines.forEach(function (l) { | ||
if (l.length === 0) { | ||
html += "\n"; | ||
} else { | ||
l.forEach(function (token) { | ||
html += "<span style=\"color: " + token.color + "\">" + escapeHtml(token.content) + "</span>"; | ||
}); | ||
html += "\n"; | ||
} | ||
}); | ||
html = html.replace(/\n*$/, ""); // Get rid of final new lines | ||
html += "</code></div></pre>"; | ||
return html; | ||
} | ||
var languages = | ||
/*#__PURE__*/ | ||
[].concat(commonLangIds, commonLangAliases, otherLangIds); | ||
/** | ||
* This gets filled in by the promise below, then should | ||
* hopefully be more or less synchronous access by each parse | ||
* of the highlighter | ||
*/ | ||
var highlighter = null; | ||
var getHighlighterObj = function getHighlighterObj(options) { | ||
if (highlighter) return highlighter; | ||
var settings = options || {}; | ||
var theme = settings.theme || "nord"; | ||
var shikiTheme; | ||
try { | ||
shikiTheme = getTheme(theme); | ||
} catch (error) { | ||
try { | ||
shikiTheme = loadTheme(theme); | ||
} catch (error) { | ||
throw new Error("Unable to load theme: " + theme + " - " + error.message); | ||
} | ||
} | ||
return getHighlighter({ | ||
theme: shikiTheme, | ||
langs: languages | ||
}).then(function (newHighlighter) { | ||
highlighter = newHighlighter; | ||
return highlighter; | ||
}); | ||
}; | ||
var defaultSettings = {}; | ||
/** | ||
* The function doing the work of transforming any codeblock samples | ||
@@ -323,8 +10,9 @@ * which have opted-in to the twoslash pattern. | ||
var visitor = function visitor(twoslashSettings) { | ||
var visitor = function visitor(highlighter, twoslashSettings) { | ||
return function (node) { | ||
var lang = node.lang; | ||
var settings = twoslashSettings || defaultSettings; // Run twoslash | ||
var settings = twoslashSettings || {}; | ||
var shouldDisableTwoslash = process && process.env && !!process.env.TWOSLASH_DISABLE; // Run twoslash | ||
runTwoSlashOnNode(settings)(node); // Shiki doesn't respect json5 as an input, so switch it | ||
if (!shouldDisableTwoslash) runTwoSlashOnNode(settings)(node); // Shiki doesn't respect json5 as an input, so switch it | ||
// to json, which can handle comments in the syntax highlight | ||
@@ -337,14 +25,20 @@ | ||
if (replacer[lang]) lang = replacer[lang]; | ||
var shouldDisableTwoslash = process && process.env && !!process.env.TWOSLASH_DISABLE; // Check we can highlight and render | ||
var results = renderCodeToHTML(node.value, lang, node.meta || [], {}, highlighter, node.twoslash); | ||
node.type = "html"; | ||
node.value = results; | ||
node.children = []; | ||
}; | ||
}; | ||
/** | ||
* Runs twoslash across an AST node, switching out the text content, and lang | ||
* and adding a `twoslash` property to the node. | ||
*/ | ||
var shouldHighlight = lang && languages.includes(lang); | ||
if (shouldHighlight && !shouldDisableTwoslash) { | ||
var tokens = highlighter.codeToThemedTokens(node.value, lang); | ||
var results = renderToHTML(tokens, { | ||
langId: lang | ||
}, node.twoslash); | ||
node.type = "html"; | ||
node.value = results; | ||
node.children = []; | ||
var runTwoSlashOnNode = function runTwoSlashOnNode(settings) { | ||
return function (node) { | ||
if (node.meta && node.meta.includes("twoslash")) { | ||
var results = runTwoSlash(node.value, node.lang, settings); | ||
node.value = results.code; | ||
node.lang = results.extension; | ||
node.twoslash = results; | ||
} | ||
@@ -359,3 +53,2 @@ }; | ||
var remarkShiki = function remarkShiki(_ref, shikiSettings, settings) { | ||
@@ -365,4 +58,4 @@ var markdownAST = _ref.markdownAST; | ||
try { | ||
return Promise.resolve(getHighlighterObj(shikiSettings)).then(function () { | ||
visit(markdownAST, "code", visitor(settings)); | ||
return Promise.resolve(createShikiHighlighter(shikiSettings)).then(function (highlighter) { | ||
visit(markdownAST, "code", visitor(highlighter, settings)); | ||
}); | ||
@@ -372,40 +65,13 @@ } catch (e) { | ||
} | ||
}; /////////////////// Mainly for internal use, but tests could use this, not considered public API, so could change | ||
/** @internal */ | ||
var runTwoSlashOnNode = function runTwoSlashOnNode(settings) { | ||
return function (node) { | ||
// Run twoslash and replace the main contents if | ||
// the ``` has 'twoslash' after it | ||
if (node.meta && node.meta.includes("twoslash")) { | ||
var map = undefined; | ||
if (settings.useNodeModules) { | ||
var laterESVersion = 6; // we don't want a hard dep on TS, so that browsers can run this code) | ||
map = createDefaultMapFromNodeModules({ | ||
target: laterESVersion | ||
}); // Add @types to the fsmap | ||
addAllFilesFromFolder(map, settings.nodeModulesTypesPath || "node_modules/@types"); | ||
} | ||
var results = twoslasher(node.value, node.lang, undefined, undefined, undefined, map); | ||
node.value = results.code; | ||
node.lang = results.extension; | ||
node.twoslash = results; | ||
} | ||
}; | ||
}; | ||
/** Sends the twoslash visitor over the existing MD AST and replaces the code samples inline, does not do highlighting */ | ||
var runTwoSlashAcrossDocument = function runTwoSlashAcrossDocument(_ref2, settings) { | ||
var markdownAST = _ref2.markdownAST; | ||
return visit(markdownAST, "code", runTwoSlashOnNode(settings || defaultSettings)); | ||
return visit(markdownAST, "code", runTwoSlashOnNode(settings || {})); | ||
}; | ||
export default remarkShiki; | ||
export { runTwoSlashAcrossDocument, runTwoSlashOnNode }; | ||
export { runTwoSlashAcrossDocument, runTwoSlashOnNode, visitor }; | ||
//# sourceMappingURL=gatsby-remark-shiki-twoslash.esm.js.map |
@@ -1,2 +0,4 @@ | ||
import { TLang } from "shiki-languages"; | ||
import type { Highlighter } from "shiki/dist/highlighter"; | ||
import type { TLang } from "shiki-languages"; | ||
import { ShikiTwoslashSettings } from "shiki-twoslash"; | ||
import { Node } from "unist"; | ||
@@ -11,7 +13,13 @@ declare type RichNode = Node & { | ||
}; | ||
declare type ShikiTwoslashSettings = { | ||
useNodeModules?: true; | ||
nodeModulesTypesPath?: string; | ||
}; | ||
/** | ||
* The function doing the work of transforming any codeblock samples | ||
* which have opted-in to the twoslash pattern. | ||
*/ | ||
export declare const visitor: (highlighter: Highlighter, twoslashSettings?: ShikiTwoslashSettings | undefined) => (node: RichNode) => void; | ||
/** | ||
* Runs twoslash across an AST node, switching out the text content, and lang | ||
* and adding a `twoslash` property to the node. | ||
*/ | ||
export declare const runTwoSlashOnNode: (settings: ShikiTwoslashSettings) => (node: RichNode) => void; | ||
/** | ||
* The main interface for the remark shiki API, sets up the | ||
@@ -22,6 +30,4 @@ * highlighter then runs a visitor across all code tags in | ||
declare const remarkShiki: ({ markdownAST }: any, shikiSettings: import("shiki/dist/highlighter").HighlighterOptions, settings: ShikiTwoslashSettings) => Promise<void>; | ||
/** @internal */ | ||
export declare const runTwoSlashOnNode: (settings: ShikiTwoslashSettings) => (node: RichNode) => void; | ||
/** Sends the twoslash visitor over the existing MD AST and replaces the code samples inline, does not do highlighting */ | ||
export declare const runTwoSlashAcrossDocument: ({ markdownAST }: any, settings?: ShikiTwoslashSettings | undefined) => any; | ||
export default remarkShiki; |
{ | ||
"name": "gatsby-remark-shiki-twoslash", | ||
"version": "0.6.1", | ||
"version": "0.7.0", | ||
"license": "MIT", | ||
"homepage": "https://github.com/microsoft/TypeScript-Website/", | ||
"description": "A remark plugin which adds twoslash code samples for TypeScript", | ||
"author": "Orta Therox", | ||
@@ -16,4 +17,3 @@ "main": "./dist/index.js", | ||
"prepublishOnly": "yarn build", | ||
"build": "tsdx build && yarn tsc src/dom.ts --outDir dist", | ||
"bootstrap": "echo 'NOOP'", | ||
"build": "tsdx build", | ||
"test": "tsdx test", | ||
@@ -23,7 +23,8 @@ "lint": "tsdx lint" | ||
"dependencies": { | ||
"@typescript/twoslash": "0.5.0", | ||
"@typescript/vfs": "1.0.1", | ||
"typescript": "*", | ||
"@typescript/twoslash": "0.6.2", | ||
"@typescript/vfs": "1.1.2", | ||
"shiki": "^0.1.6", | ||
"shiki-languages": "^0.1.6", | ||
"shiki-twoslash": "0.7.0", | ||
"typescript": "*", | ||
"unist-util-visit": "^2.0.0" | ||
@@ -30,0 +31,0 @@ }, |
@@ -179,9 +179,12 @@ ### gatsby-remark-shiki-twoslash | ||
<pre>```json | ||
```` | ||
```json | ||
{ "json": true } | ||
```</pre> | ||
``` | ||
```` | ||
If that works, then add a twoslash example: | ||
<pre>```ts twoslash | ||
```` | ||
```ts twoslash | ||
interface IdLabel {id: number, /* some fields */ } | ||
@@ -197,4 +200,5 @@ interface NameLabel {name: string, /* other fields */ } | ||
let a = createLabel("typescript");` | ||
```</pre> | ||
let a = createLabel("typescript"); | ||
``` | ||
```` | ||
@@ -201,0 +205,0 @@ If the code sample shows as |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
219
30600
7
11
169
1
+ Addedshiki-twoslash@0.7.0
+ Added@typescript/twoslash@0.6.2(transitive)
+ Added@typescript/vfs@1.1.2(transitive)
+ Addedshiki-twoslash@0.7.0(transitive)
- Removed@typescript/twoslash@0.5.0(transitive)
- Removed@typescript/vfs@1.0.1(transitive)
Updated@typescript/twoslash@0.6.2
Updated@typescript/vfs@1.1.2