From eddf3ae9f3987e01c035f4294b9a7ad7d6cf6fe3 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Sat, 25 Apr 2020 12:49:43 -0700 Subject: [PATCH 1/5] Start code span highlighting implementation --- ...kRegistry.js => createCodeNodeRegistry.js} | 10 +-- src/getPossibleThemes.js | 30 ++----- src/graphql/getCodeBlockDataFromRegistry.js | 2 +- src/graphql/highlight.js | 2 +- src/graphql/schema.d.ts | 10 +++ src/graphql/schema.graphql | 10 +++ src/index.js | 85 ++++++++++++------- ...deFenceHeader.js => parseCodeFenceInfo.js} | 4 +- src/parseCodeSpanInfo.js | 14 +++ ...gisterCodeBlock.js => registerCodeNode.js} | 46 +++++++++- src/types.d.ts | 35 +++++--- src/validateOptions.js | 10 +++ 12 files changed, 184 insertions(+), 74 deletions(-) rename src/{createCodeBlockRegistry.js => createCodeNodeRegistry.js} (96%) rename src/{parseCodeFenceHeader.js => parseCodeFenceInfo.js} (97%) create mode 100644 src/parseCodeSpanInfo.js rename src/{registerCodeBlock.js => registerCodeNode.js} (55%) diff --git a/src/createCodeBlockRegistry.js b/src/createCodeNodeRegistry.js similarity index 96% rename from src/createCodeBlockRegistry.js rename to src/createCodeNodeRegistry.js index d6e8df6..e88fcc0 100644 --- a/src/createCodeBlockRegistry.js +++ b/src/createCodeNodeRegistry.js @@ -6,10 +6,10 @@ const { declaration } = require('./renderers/css'); /** * @template TKey - * @param {CodeBlockRegistryOptions=} options - * @returns {CodeBlockRegistry} + * @param {CodeNodeRegistryOptions=} options + * @returns {CodeNodeRegistry} */ -function createCodeBlockRegistry({ prefixAllClassNames } = {}) { +function createCodeNodeRegistry({ prefixAllClassNames } = {}) { /** @type {Map} */ const nodeMap = new Map(); /** @type {ConditionalTheme[]} */ @@ -81,7 +81,7 @@ function createCodeBlockRegistry({ prefixAllClassNames } = {}) { forEachCodeBlock: nodeMap.forEach.bind(nodeMap), getAllPossibleThemes: () => themes.map(theme => ({ theme, settings: themeColors.get(theme.identifier).settings })), getTokenStylesForTheme: themeIdentifier => { - /** @type {ReturnType} */ + /** @type {ReturnType} */ const result = []; const colors = themeColors.get(themeIdentifier); const classNameMap = themeTokenClassNameMap && themeTokenClassNameMap.get(themeIdentifier); @@ -202,4 +202,4 @@ function getColorFromColorMap(colorMap, canonicalClassName) { return colorMap[index]; } -module.exports = createCodeBlockRegistry; +module.exports = createCodeNodeRegistry; diff --git a/src/getPossibleThemes.js b/src/getPossibleThemes.js index f6db886..6c10a17 100644 --- a/src/getPossibleThemes.js +++ b/src/getPossibleThemes.js @@ -7,38 +7,25 @@ const { } = require('./themeUtils'); /** - * @param {ThemeOption} themeOption + * @template {CodeBlockData | CodeSpanData} T + * @param {ThemeOption} themeOption * @param {ThemeCache} themeCache * @param {string | undefined} contextDirectory - * @param {MarkdownNode} markdownNode - * @param {MDASTNode} codeFenceNode - * @param {string} languageName - * @param {object} meta + * @param {T} codeNodeData * @returns {Promise} */ async function getPossibleThemes( themeOption, themeCache, contextDirectory, - markdownNode, - codeFenceNode, - languageName, - meta + codeNodeData ) { if (typeof themeOption === 'function') { return getPossibleThemes( - themeOption({ - markdownNode, - codeFenceNode, - language: languageName, - parsedOptions: meta - }), + themeOption(codeNodeData), themeCache, contextDirectory, - markdownNode, - codeFenceNode, - languageName, - meta + codeNodeData ); } @@ -53,10 +40,7 @@ async function getPossibleThemes( themeOption.default, themeCache, contextDirectory, - markdownNode, - codeFenceNode, - languageName, - meta + codeNodeData ); } if (themeOption.dark) { diff --git a/src/graphql/getCodeBlockDataFromRegistry.js b/src/graphql/getCodeBlockDataFromRegistry.js index d151a62..c1b83df 100644 --- a/src/graphql/getCodeBlockDataFromRegistry.js +++ b/src/graphql/getCodeBlockDataFromRegistry.js @@ -6,7 +6,7 @@ const { createTokenElement, createLineElement, createCodeBlockElement } = requir /** * @template TKey - * @param {CodeBlockRegistry} registry + * @param {CodeNodeRegistry} registry * @param {TKey} key * @param {RegisteredCodeBlockData} codeBlock * @param {() => string} getWrapperClassName diff --git a/src/graphql/highlight.js b/src/graphql/highlight.js index 5a04dd2..7709db4 100644 --- a/src/graphql/highlight.js +++ b/src/graphql/highlight.js @@ -40,7 +40,7 @@ async function highlight(args, pluginOptions, { cache, createNodeId }) { const grammarCache = await cache.get('grammars'); const possibleThemes = await getThemes(theme, args, themeCache); const scope = getScope(args.language, grammarCache, languageAliases); - /** @type {CodeBlockRegistry} */ + /** @type {CodeNodeRegistry} */ const codeBlockRegistry = createCodeBlockRegistry({ prefixAllClassNames: true }); const meta = parseCodeFenceHeader(args.language, args.meta); diff --git a/src/graphql/schema.d.ts b/src/graphql/schema.d.ts index 277601f..f6dbddb 100644 --- a/src/graphql/schema.d.ts +++ b/src/graphql/schema.d.ts @@ -48,6 +48,16 @@ declare namespace grvsc { additionalThemes: GRVSCTheme[]; tokenizedLines?: GRVSCTokenizedLine[]; } + interface GRVSCCodeSpan extends Node { + index: number; + html: string; + text: string; + className: string; + language?: string; + defaultTheme: GRVSCTheme; + additionalThemes: GRVSCTheme[]; + tokens: GRVSCToken[]; + } interface GRVSCStylesheet extends Node { css: string; } diff --git a/src/graphql/schema.graphql b/src/graphql/schema.graphql index 7e9e192..18a6244 100644 --- a/src/graphql/schema.graphql +++ b/src/graphql/schema.graphql @@ -51,6 +51,16 @@ type GRVSCCodeBlock implements Node { additionalThemes: [GRVSCTheme!]! tokenizedLines: [GRVSCTokenizedLine!] } +type GRVSCCodeSpan implements Node { + index: Int! + html: String! + text: String! + className: String! + language: String + defaultTheme: GRVSCTheme! + additionalThemes: [GRVSCTheme!]! + tokens: [GRVSCToken!]! +} type GRVSCStylesheet implements Node { css: String! } diff --git a/src/index.js b/src/index.js index 850649e..a6aad21 100644 --- a/src/index.js +++ b/src/index.js @@ -5,12 +5,13 @@ const logger = require('loglevel'); const visit = require('unist-util-visit'); const setup = require('./setup'); const createGetRegistry = require('./createGetRegistry'); -const registerCodeBlock = require('./registerCodeBlock'); const getPossibleThemes = require('./getPossibleThemes'); -const createCodeBlockRegistry = require('./createCodeBlockRegistry'); -const parseCodeFenceHeader = require('./parseCodeFenceHeader'); +const createCodeNodeRegistry = require('./createCodeNodeRegistry'); +const parseCodeFenceInfo = require('./parseCodeFenceInfo'); +const parseCodeSpanInfo = require('./parseCodeSpanInfo'); const createSchemaCustomization = require('./graphql/createSchemaCustomization'); const getCodeBlockGraphQLDataFromRegistry = require('./graphql/getCodeBlockDataFromRegistry'); +const { registerCodeBlock, registerCodeSpan } = require('./registerCodeNode'); const { createHash } = require('crypto'); const { setChildNodes } = require('./cacheUtils'); const { getScope } = require('./storeUtils'); @@ -38,6 +39,7 @@ function createPlugin() { replaceColor, logLevel, getLineTransformers, + inlineCode, ...rest } = await once(() => setup(options, cache), 'setup'); @@ -51,6 +53,7 @@ function createPlugin() { injectStyles, replaceColor, logLevel, + inlineCode, ...rest }, cache @@ -58,9 +61,9 @@ function createPlugin() { // 1. Gather all code fence nodes from Markdown AST. - /** @type {MDASTNode[]} */ + /** @type {(MDASTNode<'code'> | MDASTNode<'inlineCode'>)[]} */ const nodes = []; - visit(markdownAST, 'code', node => { + visit(markdownAST, ({ type }) => type === 'code' || type === 'inlineCode', node => { nodes.push(node); }); @@ -70,13 +73,20 @@ function createPlugin() { /** @type {grvsc.gql.GRVSCCodeBlock[]} */ const graphQLNodes = []; - /** @type {CodeBlockRegistry} */ - const codeBlockRegistry = createCodeBlockRegistry(); + /** @type {CodeNodeRegistry} */ + const codeNodeRegistry = createCodeNodeRegistry(); for (const node of nodes) { /** @type {string} */ const text = node.value || (node.children && node.children[0] && node.children[0].value); if (!text) continue; - const { languageName, meta } = parseCodeFenceHeader(node.lang ? node.lang.toLowerCase() : '', node.meta); + const { languageName, meta, text: parsedText = text } = node.type === 'code' + ? parseCodeFenceInfo(node.lang ? node.lang.toLowerCase() : '', node.meta) + : parseCodeSpanInfo(text, inlineCode.marker); + + if (node.type === 'inlineCode' && !languageName) { + continue; + } + const grammarCache = await cache.get('grammars'); const scope = getScope(languageName, grammarCache, languageAliases); if (!scope && languageName) { @@ -87,29 +97,45 @@ function createPlugin() { ); } + const nodeData = /** @type {CodeBlockData | CodeSpanData} */ ({ + node, + markdownNode, + language: languageName + }); + const possibleThemes = await getPossibleThemes( - theme, + node.type === 'inlineCode' ? inlineCode.theme || theme : theme, await cache.get('themes'), // Node could be sourced from something other than a File node markdownNode.fileAbsolutePath ? path.dirname(markdownNode.fileAbsolutePath) : undefined, - markdownNode, - node, - languageName, - meta + nodeData ); - await registerCodeBlock( - codeBlockRegistry, - node, - possibleThemes, - () => getRegistry(cache, scope), - lineTransformers, - scope, - text, - languageName, - meta, - cache - ); + if (node.type === 'inlineCode') { + await registerCodeSpan( + codeNodeRegistry, + node, + possibleThemes, + () => getRegistry(cache, scope), + scope, + parsedText, + languageName, + cache + ); + } else { + await registerCodeBlock( + codeNodeRegistry, + node, + possibleThemes, + () => getRegistry(cache, scope), + lineTransformers, + scope, + parsedText, + languageName, + meta, + cache + ); + } } // 3. For each code block registered, convert its tokenization and theme data @@ -117,9 +143,9 @@ function createPlugin() { // time, change the original code fence Markdown node to an HTML node and set // its value to the HTML rendering contained in the GraphQL node. - codeBlockRegistry.forEachCodeBlock((codeBlock, node) => { + codeNodeRegistry.forEachCodeBlock((codeBlock, node) => { const graphQLNode = getCodeBlockGraphQLDataFromRegistry( - codeBlockRegistry, + codeNodeRegistry, node, codeBlock, getWrapperClassName, @@ -148,6 +174,7 @@ function createPlugin() { ? wrapperClassName({ language: codeBlock.languageName, markdownNode, + node, codeFenceNode: node, parsedOptions: codeBlock.meta }) @@ -159,8 +186,8 @@ function createPlugin() { // then append that CSS to the Markdown AST in an HTML node. const styleElement = createStyleElement( - codeBlockRegistry.getAllPossibleThemes(), - codeBlockRegistry.getTokenStylesForTheme, + codeNodeRegistry.getAllPossibleThemes(), + codeNodeRegistry.getTokenStylesForTheme, replaceColor, injectStyles ? styles : undefined ); diff --git a/src/parseCodeFenceHeader.js b/src/parseCodeFenceInfo.js similarity index 97% rename from src/parseCodeFenceHeader.js rename to src/parseCodeFenceInfo.js index 063b5c5..1c26ffc 100644 --- a/src/parseCodeFenceHeader.js +++ b/src/parseCodeFenceInfo.js @@ -17,7 +17,7 @@ function test(input, pattern) { * @param {string} lang * @param {string=} metaString */ -function parseCodeFenceHeader(lang, metaString) { +function parseCodeFenceInfo(lang, metaString) { let pos = 0; let meta = {}; let languageName = ''; @@ -170,4 +170,4 @@ function parseCodeFenceHeader(lang, metaString) { } } -module.exports = parseCodeFenceHeader; +module.exports = parseCodeFenceInfo; diff --git a/src/parseCodeSpanInfo.js b/src/parseCodeSpanInfo.js new file mode 100644 index 0000000..8efd378 --- /dev/null +++ b/src/parseCodeSpanInfo.js @@ -0,0 +1,14 @@ +/** + * @param {string} text + * @param {string} delimiter + * @returns {{ languageName: string | undefined, meta: undefined, text: string }} + */ +function parseCodeSpanInfo(text, delimiter) { + const index = text.indexOf(delimiter); + if (index <= 0) return { languageName: undefined, meta: undefined, text }; + const languageName = text.slice(0, index).trim().toLowerCase(); + if (!languageName) return { languageName: undefined, meta: undefined, text }; + return { languageName, meta: undefined, text: text.slice(index + delimiter.length) }; +} + +module.exports = parseCodeSpanInfo; diff --git a/src/registerCodeBlock.js b/src/registerCodeNode.js similarity index 55% rename from src/registerCodeBlock.js rename to src/registerCodeNode.js index f15684a..7d0a212 100644 --- a/src/registerCodeBlock.js +++ b/src/registerCodeNode.js @@ -4,7 +4,7 @@ const { getGrammar } = require('./storeUtils'); /** * @template TKey - * @param {CodeBlockRegistry} codeBlockRegistry + * @param {CodeNodeRegistry} codeBlockRegistry * @param {TKey} registryKey * @param {ConditionalTheme[]} possibleThemes * @param {() => Promise<[import('vscode-textmate').Registry, () => void]>} getTextMateRegistry @@ -57,4 +57,46 @@ async function registerCodeBlock( } } -module.exports = registerCodeBlock; +/** + * @template TKey + * @param {CodeNodeRegistry} codeBlockRegistry + * @param {TKey} registryKey + * @param {ConditionalTheme[]} possibleThemes + * @param {() => Promise<[import('vscode-textmate').Registry, () => void]>} getTextMateRegistry + * @param {string} scope + * @param {string} text + * @param {string | undefined} languageName + * @param {GatsbyCache} cache + */ +async function registerCodeSpan( + codeBlockRegistry, + registryKey, + possibleThemes, + getTextMateRegistry, + scope, + text, + languageName, + cache +) { + const grammarCache = await cache.get('grammars'); + const [registry, unlockRegistry] = await getTextMateRegistry(); + try { + /** @type {Line[]} */ + const lines = [{ text, data: {}, attrs: {} }]; + const { tokenTypes, languageId } = getGrammar(scope, grammarCache); + const grammar = await registry.loadGrammarWithConfiguration(scope, languageId, { tokenTypes }); + codeBlockRegistry.register(registryKey, { + lines, + text, + meta: {}, + languageName, + possibleThemes, + isTokenized: true, + tokenizationResults: possibleThemes.map(theme => tokenizeWithTheme(lines, theme, grammar, registry)) + }); + } finally { + unlockRegistry(); + } +} + +module.exports = { registerCodeBlock, registerCodeSpan }; diff --git a/src/types.d.ts b/src/types.d.ts index fa83e59..d935048 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -9,13 +9,21 @@ interface RemarkPluginArguments { createNodeId: (key: string) => string; } -interface CodeFenceData { - language: string; +interface CodeBlockData { + language?: string; markdownNode: MarkdownNode; - codeFenceNode: any; + /** @deprecated Use `node` instead. */ + codeFenceNode: MDASTNode<'code'>; + node: MDASTNode<'code'>; parsedOptions: any; } +interface CodeSpanData { + language?: string; + markdownNode: MarkdownNode; + node: MDASTNode<'inlineCode'>; +} + interface LineData { /** The line’s string content */ content: string; @@ -49,13 +57,13 @@ interface Host { decompress: (input: string | Buffer, output: string) => Promise; } -type LegacyThemeOption = string | LegacyThemeSettings | ((data: CodeFenceData) => string | LegacyThemeSettings); -type ThemeOption = string | ThemeSettings | ((data: CodeFenceData) => string | ThemeSettings); +type LegacyThemeOption = string | LegacyThemeSettings | ((data: CodeBlockData) => string | LegacyThemeSettings); +type ThemeOption = string | ThemeSettings | ((data: T) => string | ThemeSettings); interface PluginOptions { - theme?: ThemeOption; + theme?: ThemeOption; colorTheme?: LegacyThemeOption; - wrapperClassName?: string | ((data: CodeFenceData) => string); + wrapperClassName?: string | ((data: CodeBlockData) => string); languageAliases?: Record; extensions?: string[]; getLineClassName?: (line: LineData) => string; @@ -64,6 +72,11 @@ interface PluginOptions { logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error'; host?: Host; getLineTransformers?: (pluginOptions: PluginOptions, cache: GatsbyCache) => LineTransformer[]; + inlineCode?: { + theme?: ThemeOption; + marker: string; + className?: string | ((data: CodeSpanData) => string | undefined); + } } interface GrammarData { @@ -150,8 +163,8 @@ interface RegisteredToken { additionalThemeTokenData: grvsc.gql.GRVSCThemeTokenData[]; } -interface MDASTNode { - type: string; +interface MDASTNode { + type: T; lang?: string; meta?: string; value?: string; @@ -168,7 +181,7 @@ type Line = { data: object; }; -interface CodeBlockRegistry { +interface CodeNodeRegistry { register: (key: TKey, data: Omit) => void; forEachLine: (codeBlockKey: TKey, action: (line: Line, index: number, lines: Line[]) => void) => void; forEachToken: ( @@ -181,7 +194,7 @@ interface CodeBlockRegistry { getTokenStylesForTheme: (themeIdentifier: string) => { className: string, css: grvsc.CSSDeclaration[] }[]; } -interface CodeBlockRegistryOptions { +interface CodeNodeRegistryOptions { prefixAllClassNames?: boolean; } diff --git a/src/validateOptions.js b/src/validateOptions.js index c29ea78..e2033cd 100644 --- a/src/validateOptions.js +++ b/src/validateOptions.js @@ -1,6 +1,8 @@ const { EOL } = require('os'); const { deprecationNotice, isRelativePath } = require('./utils'); +const validMarkerRegExp = /^[^\sa-zA-Z0-9.-_`\\<]+$/; + /** * @param {PluginOptions} options */ @@ -32,6 +34,14 @@ function validateOptions(options) { }); } + if (options.inlineCode) { + if (!('marker' in options.inlineCode)) { + addError('inlineCode', `Key 'marker' is required.`); + } else if (typeof options.inlineCode.marker !== 'string' || !validMarkerRegExp.test(options.inlineCode.marker)) { + addError('inlineCode.marker', `Marker must be a string without whitespace, ASCII letters or numerals, or character: .-_\`\\<`); + } + } + if (errors.length) { throw new Error(errors.join(EOL.repeat(2))); } From c61b59f6feddcb5b849eeba7d846c01eb54b1200 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Sun, 26 Apr 2020 14:23:30 -0700 Subject: [PATCH 2/5] Finish main plugin implementation --- src/createCodeNodeRegistry.js | 29 ++++++---- src/factory/html.js | 15 +++++- src/getPossibleThemes.js | 21 ++------ src/graphql/getCodeBlockDataFromRegistry.js | 4 +- src/graphql/getCodeSpanDataFromRegistry.js | 48 +++++++++++++++++ src/graphql/getThemes.js | 4 +- src/graphql/highlight.js | 18 ++++--- src/index.js | 59 +++++++++++++++++---- src/parseCodeSpanInfo.js | 5 +- src/registerCodeNode.js | 4 +- src/themeUtils.js | 2 +- src/types.d.ts | 13 +++-- src/validateOptions.js | 5 +- test/parseCodeFenceHeader.test.js | 4 +- 14 files changed, 170 insertions(+), 61 deletions(-) create mode 100644 src/graphql/getCodeSpanDataFromRegistry.js diff --git a/src/createCodeNodeRegistry.js b/src/createCodeNodeRegistry.js index e88fcc0..c514f1b 100644 --- a/src/createCodeNodeRegistry.js +++ b/src/createCodeNodeRegistry.js @@ -5,13 +5,15 @@ const { getTokenDataFromMetadata } = require('../lib/vscode/modes'); const { declaration } = require('./renderers/css'); /** - * @template TKey + * @template {Keyable} TKey * @param {CodeNodeRegistryOptions=} options * @returns {CodeNodeRegistry} */ function createCodeNodeRegistry({ prefixAllClassNames } = {}) { - /** @type {Map} */ - const nodeMap = new Map(); + /** @type {Map} */ + const blockMap = new Map(); + /** @type {Map} */ + const spanMap = new Map(); /** @type {ConditionalTheme[]} */ let themes = []; /** @type {Map }>} */ @@ -23,16 +25,17 @@ function createCodeNodeRegistry({ prefixAllClassNames } = {}) { return { register: (key, data) => { - nodeMap.set(key, { ...data, index: nodeMap.size }); + const map = key.type === 'code' ? blockMap : spanMap; + map.set(key, { ...data, index: map.size }); themes = concatConditionalThemes(themes, data.possibleThemes); data.tokenizationResults.forEach(({ theme, colorMap, settings }) => themeColors.set(theme.identifier, { colorMap, settings }) ); }, - forEachLine: (node, action) => nodeMap.get(node).lines.forEach(action), + forEachLine: (node, action) => blockMap.get(node).lines.forEach(action), forEachToken: (node, lineIndex, tokenAction) => { generateClassNames(); - const { tokenizationResults, isTokenized, lines } = nodeMap.get(node); + const { tokenizationResults, isTokenized, lines } = blockMap.get(node); if (!isTokenized) { return; } @@ -78,7 +81,8 @@ function createCodeNodeRegistry({ prefixAllClassNames } = {}) { }); }); }, - forEachCodeBlock: nodeMap.forEach.bind(nodeMap), + forEachCodeBlock: blockMap.forEach.bind(blockMap), + forEachCodeSpan: spanMap.forEach.bind(spanMap), getAllPossibleThemes: () => themes.map(theme => ({ theme, settings: themeColors.get(theme.identifier).settings })), getTokenStylesForTheme: themeIdentifier => { /** @type {ReturnType} */ @@ -113,7 +117,14 @@ function createCodeNodeRegistry({ prefixAllClassNames } = {}) { if (themeTokenClassNameMap) return; themeTokenClassNameMap = new Map(); zippedLines = new Map(); - nodeMap.forEach(({ lines, tokenizationResults, isTokenized }, node) => { + blockMap.forEach(generate); + spanMap.forEach(generate); + + /** + * @param {RegisteredCodeNodeData} data + * @param {TKey} node + */ + function generate({ lines, tokenizationResults, isTokenized }, node) { if (!isTokenized) return; /** @type {Token[][][]} */ const zippedLinesForNode = []; @@ -141,7 +152,7 @@ function createCodeNodeRegistry({ prefixAllClassNames } = {}) { }); }); }); - }); + } } } diff --git a/src/factory/html.js b/src/factory/html.js index 1061627..b741b75 100644 --- a/src/factory/html.js +++ b/src/factory/html.js @@ -33,6 +33,18 @@ function createLineElement(line, meta, index, language, getLineClassName, tokens return span(attrs, children, { whitespace: TriviaRenderFlags.NoWhitespace }); } +/** + * @param {number} index + * @param {string} language + * @param {string | undefined} className + * @param {grvsc.HTMLElement[]} tokens + */ +function createCodeSpanElement(index, language, className, tokens) { + return code({ class: className, 'data-language': language, 'data-index': index }, tokens, { + whitespace: TriviaRenderFlags.NoWhitespace + }); +} + /** * Returns the token element array with contiguous spans having the same class name * merged into a single span to minimize the number of elements returned. @@ -90,5 +102,6 @@ module.exports = { createTokenElement, createLineElement, createCodeBlockElement, - createStyleElement + createStyleElement, + createCodeSpanElement }; diff --git a/src/getPossibleThemes.js b/src/getPossibleThemes.js index 6c10a17..b75d876 100644 --- a/src/getPossibleThemes.js +++ b/src/getPossibleThemes.js @@ -14,19 +14,9 @@ const { * @param {T} codeNodeData * @returns {Promise} */ -async function getPossibleThemes( - themeOption, - themeCache, - contextDirectory, - codeNodeData -) { +async function getPossibleThemes(themeOption, themeCache, contextDirectory, codeNodeData) { if (typeof themeOption === 'function') { - return getPossibleThemes( - themeOption(codeNodeData), - themeCache, - contextDirectory, - codeNodeData - ); + return getPossibleThemes(themeOption(codeNodeData), themeCache, contextDirectory, codeNodeData); } if (typeof themeOption === 'string') { @@ -36,12 +26,7 @@ async function getPossibleThemes( /** @type {ConditionalTheme[]} */ let themes; if (themeOption.default) { - themes = await getPossibleThemes( - themeOption.default, - themeCache, - contextDirectory, - codeNodeData - ); + themes = await getPossibleThemes(themeOption.default, themeCache, contextDirectory, codeNodeData); } if (themeOption.dark) { themes = concatConditionalThemes(themes, [ diff --git a/src/graphql/getCodeBlockDataFromRegistry.js b/src/graphql/getCodeBlockDataFromRegistry.js index c1b83df..1e22f87 100644 --- a/src/graphql/getCodeBlockDataFromRegistry.js +++ b/src/graphql/getCodeBlockDataFromRegistry.js @@ -5,10 +5,10 @@ const { flatMap, partitionOne, escapeHTML } = require('../utils'); const { createTokenElement, createLineElement, createCodeBlockElement } = require('../factory/html'); /** - * @template TKey + * @template {Keyable} TKey * @param {CodeNodeRegistry} registry * @param {TKey} key - * @param {RegisteredCodeBlockData} codeBlock + * @param {RegisteredCodeNodeData} codeBlock * @param {() => string} getWrapperClassName * @param {(line: LineData) => string} getLineClassName * @returns {Omit} diff --git a/src/graphql/getCodeSpanDataFromRegistry.js b/src/graphql/getCodeSpanDataFromRegistry.js new file mode 100644 index 0000000..9ddf2fb --- /dev/null +++ b/src/graphql/getCodeSpanDataFromRegistry.js @@ -0,0 +1,48 @@ +const { renderHTML } = require('../renderers/html'); +const { partitionOne } = require('../utils'); +const { createTokenElement, createCodeSpanElement } = require('../factory/html'); + +/** + * @template {Keyable} TKey + * @param {CodeNodeRegistry} registry + * @param {TKey} key + * @param {RegisteredCodeNodeData} codeSpan + * @param {() => string} getClassName + * @returns {Omit} + */ +function getCodeSpanDataFromRegistry(registry, key, codeSpan, getClassName) { + const { index, languageName, text, possibleThemes } = codeSpan; + /** @type {grvsc.HTMLElement[]} */ + const tokenElements = []; + /** @type {grvsc.gql.GRVSCToken[]} */ + const gqlTokens = []; + + registry.forEachToken(key, 0, token => { + const html = createTokenElement(token); + tokenElements.push(html); + gqlTokens.push({ + ...token, + className: html.attributes.class, + html: renderHTML(html) + }); + }); + + const className = getClassName(); + const html = createCodeSpanElement(index, languageName, className, tokenElements); + const [defaultTheme, additionalThemes] = partitionOne(possibleThemes, t => + t.conditions.some(c => c.condition === 'default') + ); + + return { + index, + text, + html: renderHTML(html), + language: languageName, + className: getClassName(), + defaultTheme, + additionalThemes, + tokens: gqlTokens + }; +} + +module.exports = getCodeSpanDataFromRegistry; diff --git a/src/graphql/getThemes.js b/src/graphql/getThemes.js index efba953..a20af7f 100644 --- a/src/graphql/getThemes.js +++ b/src/graphql/getThemes.js @@ -17,7 +17,7 @@ async function convertThemeArgument(theme, themeCache) { } /** - * @param {ThemeOption} themeOption + * @param {ThemeOption} themeOption * @param {grvsc.gql.CSSArgs} args * @param {ThemeCache} themeCache * @returns {Promise} @@ -40,7 +40,7 @@ async function getThemes(themeOption, args, themeCache) { `evaluating 'grvscHighlight'.` ); } - return getPossibleThemes(themeOption, themeCache, undefined, undefined, undefined, undefined, undefined); + return getPossibleThemes(themeOption, themeCache, undefined, undefined); } module.exports = getThemes; diff --git a/src/graphql/highlight.js b/src/graphql/highlight.js index 7709db4..d972cda 100644 --- a/src/graphql/highlight.js +++ b/src/graphql/highlight.js @@ -1,13 +1,14 @@ const setup = require('../setup'); const plugin = require('../../index'); -const registerCodeBlock = require('../registerCodeBlock'); -const parseCodeFenceHeader = require('../parseCodeFenceHeader'); -const createCodeBlockRegistry = require('../createCodeBlockRegistry'); +const { registerCodeBlock } = require('../registerCodeNode'); +const parseCodeFenceHeader = require('../parseCodeFenceInfo'); +const createCodeNodeRegistry = require('../createCodeNodeRegistry'); const getCodeBlockDataFromRegistry = require('./getCodeBlockDataFromRegistry'); const getThemes = require('./getThemes'); const { createHash } = require('crypto'); const { getScope } = require('../storeUtils'); -const registryKey = 0; +/** @type {{ type: 'code' }} */ +const registryKey = { type: 'code' }; /** * @param {grvsc.gql.HighlightArgs} args @@ -41,11 +42,11 @@ async function highlight(args, pluginOptions, { cache, createNodeId }) { const possibleThemes = await getThemes(theme, args, themeCache); const scope = getScope(args.language, grammarCache, languageAliases); /** @type {CodeNodeRegistry} */ - const codeBlockRegistry = createCodeBlockRegistry({ prefixAllClassNames: true }); + const codeNodeRegistry = createCodeNodeRegistry({ prefixAllClassNames: true }); const meta = parseCodeFenceHeader(args.language, args.meta); await registerCodeBlock( - codeBlockRegistry, + codeNodeRegistry, registryKey, possibleThemes, () => plugin.getRegistry(cache, scope), @@ -59,9 +60,9 @@ async function highlight(args, pluginOptions, { cache, createNodeId }) { /** @type {Omit} */ let result; - codeBlockRegistry.forEachCodeBlock(codeBlock => { + codeNodeRegistry.forEachCodeBlock(codeBlock => { result = getCodeBlockDataFromRegistry( - codeBlockRegistry, + codeNodeRegistry, registryKey, codeBlock, getWrapperClassName, @@ -72,6 +73,7 @@ async function highlight(args, pluginOptions, { cache, createNodeId }) { return typeof wrapperClassName === 'function' ? wrapperClassName({ language: codeBlock.languageName, + node: undefined, markdownNode: undefined, codeFenceNode: undefined, parsedOptions: codeBlock.meta diff --git a/src/index.js b/src/index.js index a6aad21..154a772 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,7 @@ const parseCodeFenceInfo = require('./parseCodeFenceInfo'); const parseCodeSpanInfo = require('./parseCodeSpanInfo'); const createSchemaCustomization = require('./graphql/createSchemaCustomization'); const getCodeBlockGraphQLDataFromRegistry = require('./graphql/getCodeBlockDataFromRegistry'); +const getCodeSpanGraphQLDataFromRegistry = require('./graphql/getCodeSpanDataFromRegistry'); const { registerCodeBlock, registerCodeSpan } = require('./registerCodeNode'); const { createHash } = require('crypto'); const { setChildNodes } = require('./cacheUtils'); @@ -63,25 +64,30 @@ function createPlugin() { /** @type {(MDASTNode<'code'> | MDASTNode<'inlineCode'>)[]} */ const nodes = []; - visit(markdownAST, ({ type }) => type === 'code' || type === 'inlineCode', node => { - nodes.push(node); - }); + visit( + markdownAST, + ({ type }) => type === 'code' || type === 'inlineCode', + node => { + nodes.push(node); + } + ); // 2. For each code fence found, parse its header, determine what themes it will use, // and register its contents with a central code block registry, performing tokenization // along the way. - /** @type {grvsc.gql.GRVSCCodeBlock[]} */ + /** @type {(grvsc.gql.GRVSCCodeBlock | grvsc.gql.GRVSCCodeSpan)[]} */ const graphQLNodes = []; - /** @type {CodeNodeRegistry} */ + /** @type {CodeNodeRegistry>} */ const codeNodeRegistry = createCodeNodeRegistry(); for (const node of nodes) { /** @type {string} */ const text = node.value || (node.children && node.children[0] && node.children[0].value); if (!text) continue; - const { languageName, meta, text: parsedText = text } = node.type === 'code' - ? parseCodeFenceInfo(node.lang ? node.lang.toLowerCase() : '', node.meta) - : parseCodeSpanInfo(text, inlineCode.marker); + const { languageName, meta, text: parsedText = text } = + node.type === 'code' + ? parseCodeFenceInfo(node.lang ? node.lang.toLowerCase() : '', node.meta) + : parseCodeSpanInfo(text, inlineCode.marker); if (node.type === 'inlineCode' && !languageName) { continue; @@ -138,7 +144,7 @@ function createPlugin() { } } - // 3. For each code block registered, convert its tokenization and theme data + // 3. For each code block/span registered, convert its tokenization and theme data // to a GraphQL-compatible representation, including HTML renderings. At the same // time, change the original code fence Markdown node to an HTML node and set // its value to the HTML rendering contained in the GraphQL node. @@ -153,7 +159,8 @@ function createPlugin() { ); // Update Markdown node - node.type = 'html'; + /** @type {MDASTNode} */ + (node).type = 'html'; node.value = graphQLNode.html; // Push GraphQL node @@ -182,6 +189,38 @@ function createPlugin() { } }); + codeNodeRegistry.forEachCodeSpan((codeSpan, node) => { + const graphQLNode = getCodeSpanGraphQLDataFromRegistry(codeNodeRegistry, node, codeSpan, getClassName); + + // Update Markdown node + /** @type {MDASTNode} */ + (node).type = 'html'; + node.value = graphQLNode.html; + + // Push GraphQL node + graphQLNodes.push({ + ...graphQLNode, + id: createNodeId(`GRVSCCodeSpan-${markdownNode.id}-${codeSpan.index}`), + parent: markdownNode.id, + internal: { + type: 'GRVSCCodeSpan', + contentDigest: createHash('md5') + .update(JSON.stringify(graphQLNode)) + .digest('hex') + } + }); + + function getClassName() { + return typeof inlineCode.className === 'function' + ? inlineCode.className({ + language: codeSpan.languageName, + markdownNode, + node + }) + : inlineCode.className; + } + }); + // 4. Generate CSS rules for each theme used by one or more code blocks in the registry, // then append that CSS to the Markdown AST in an HTML node. diff --git a/src/parseCodeSpanInfo.js b/src/parseCodeSpanInfo.js index 8efd378..01a196d 100644 --- a/src/parseCodeSpanInfo.js +++ b/src/parseCodeSpanInfo.js @@ -6,7 +6,10 @@ function parseCodeSpanInfo(text, delimiter) { const index = text.indexOf(delimiter); if (index <= 0) return { languageName: undefined, meta: undefined, text }; - const languageName = text.slice(0, index).trim().toLowerCase(); + const languageName = text + .slice(0, index) + .trim() + .toLowerCase(); if (!languageName) return { languageName: undefined, meta: undefined, text }; return { languageName, meta: undefined, text: text.slice(index + delimiter.length) }; } diff --git a/src/registerCodeNode.js b/src/registerCodeNode.js index 7d0a212..d6c6704 100644 --- a/src/registerCodeNode.js +++ b/src/registerCodeNode.js @@ -3,7 +3,7 @@ const { getTransformedLines } = require('./transformers'); const { getGrammar } = require('./storeUtils'); /** - * @template TKey + * @template {Keyable} TKey * @param {CodeNodeRegistry} codeBlockRegistry * @param {TKey} registryKey * @param {ConditionalTheme[]} possibleThemes @@ -58,7 +58,7 @@ async function registerCodeBlock( } /** - * @template TKey + * @template {Keyable} TKey * @param {CodeNodeRegistry} codeBlockRegistry * @param {TKey} registryKey * @param {ConditionalTheme[]} possibleThemes diff --git a/src/themeUtils.js b/src/themeUtils.js index b76f705..ac4fe64 100644 --- a/src/themeUtils.js +++ b/src/themeUtils.js @@ -205,7 +205,7 @@ function getStylesFromThemeSettings(settings) { /** * @param {LegacyThemeOption} themeOption - * @returns {ThemeOption} + * @returns {ThemeOption} */ function convertLegacyThemeOption(themeOption) { if (typeof themeOption === 'function') { diff --git a/src/types.d.ts b/src/types.d.ts index d935048..415bfaa 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -143,7 +143,7 @@ interface ConditionalTheme { type RawTheme = import('vscode-textmate').IRawTheme & { resultColors: Record }; -interface RegisteredCodeBlockData { +interface RegisteredCodeNodeData { index: number; meta: object; text: string; @@ -163,6 +163,10 @@ interface RegisteredToken { additionalThemeTokenData: grvsc.gql.GRVSCThemeTokenData[]; } +interface Keyable { + type: 'code' | 'inlineCode'; +} + interface MDASTNode { type: T; lang?: string; @@ -181,15 +185,16 @@ type Line = { data: object; }; -interface CodeNodeRegistry { - register: (key: TKey, data: Omit) => void; +interface CodeNodeRegistry { + register: (key: TKey, data: Omit) => void; forEachLine: (codeBlockKey: TKey, action: (line: Line, index: number, lines: Line[]) => void) => void; forEachToken: ( key: TKey, lineIndex: number, tokenAction: (token: RegisteredToken) => void ) => void; - forEachCodeBlock: (action: (data: RegisteredCodeBlockData & { index: number }, codeBlockKey: TKey) => void) => void; + forEachCodeBlock: (action: (data: RegisteredCodeNodeData, codeBlockKey: TKey & { type: 'code' }) => void) => void; + forEachCodeSpan: (action: (data: RegisteredCodeNodeData, codeSpanKey: TKey & { type: 'inlineCode' }) => void) => void; getAllPossibleThemes: () => { theme: ConditionalTheme, settings: Record }[]; getTokenStylesForTheme: (themeIdentifier: string) => { className: string, css: grvsc.CSSDeclaration[] }[]; } diff --git a/src/validateOptions.js b/src/validateOptions.js index e2033cd..fc4a7d4 100644 --- a/src/validateOptions.js +++ b/src/validateOptions.js @@ -38,7 +38,10 @@ function validateOptions(options) { if (!('marker' in options.inlineCode)) { addError('inlineCode', `Key 'marker' is required.`); } else if (typeof options.inlineCode.marker !== 'string' || !validMarkerRegExp.test(options.inlineCode.marker)) { - addError('inlineCode.marker', `Marker must be a string without whitespace, ASCII letters or numerals, or character: .-_\`\\<`); + addError( + 'inlineCode.marker', + `Marker must be a string without whitespace, ASCII letters or numerals, or character: .-_\`\\<` + ); } } diff --git a/test/parseCodeFenceHeader.test.js b/test/parseCodeFenceHeader.test.js index 8331f5c..e1ec041 100644 --- a/test/parseCodeFenceHeader.test.js +++ b/test/parseCodeFenceHeader.test.js @@ -1,7 +1,7 @@ // @ts-check -const parse = require('../src/parseCodeFenceHeader'); +const parse = require('../src/parseCodeFenceInfo'); -describe('parseCodeFenceHeader', () => { +describe('parseCodeFenceInfo', () => { it('parses language name without meta', () => { expect(parse('jsx')).toEqual({ languageName: 'jsx', meta: {} }); expect(parse('c++')).toEqual({ languageName: 'c++', meta: {} }); From 86645386628ffc9bd0fb4135403a553f07cb6c69 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Sun, 26 Apr 2020 14:29:18 -0700 Subject: [PATCH 3/5] Add new GraphQL resolver --- gatsby-node.js | 88 ++++++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/gatsby-node.js b/gatsby-node.js index 2cd5505..59c9ac8 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -13,45 +13,15 @@ exports.createResolvers = ({ grvscCodeBlocks: { type: ['GRVSCCodeBlock'], resolve(source, _, context) { - return getFromCache(); - - /** @param {boolean=} stop */ - async function getFromCache(stop) { - const childNodes = await getChildNodes(cache, source.id, source.internal.contentDigest); - // Hack alert: ensure plugin has been run by querying htmlAst, - // which is set via `setFieldsOnGraphQLNodeType` by gatsby-transformer-remark, - // therefore might not have been run before this resolver runs. - if (!childNodes && !stop) { - await context.nodeModel.runQuery({ - query: { - filter: { - id: { eq: source.id }, - htmlAst: { ne: null }, - }, - }, - type: 'MarkdownRemark', - firstOnly: true, - }); - return getFromCache(true); - } - if (!childNodes) { - logger.error( - 'gatsby-remark-vscode couldn’t retrieve up-to-date GRVSCCodeBlock GraphQL nodes. ' + - 'The `GRVSCCodeBlocks` field may be missing, empty or stale. ' + - 'The Gatsby cache is probably in a weird state. Try running `gatsby clean`, and file an ' + - 'issue at https://github.com/andrewbranch/gatsby-remark-vscode/issues/new if the problem persists.' - ); - - return context.nodeModel.runQuery({ - query: { parent: { id: { eq: source.id } } }, - type: 'GRVSCCodeBlock', - firstOnly: false - }); - } - return childNodes || []; - } - }, + return getFromCache('GRVSCCodeBlock', cache, source, context); + } }, + grvscCodeSpans: { + type: ['GRVSCCodeSpan'], + resolve(source, _, context) { + return getFromCache('GRVSCCodeSpan', cache, source, context); + } + } }, Query: { @@ -83,3 +53,45 @@ exports.createResolvers = ({ } }); }; + +/** + * @param {string} type + * @param {any} cache + * @param {any} source + * @param {any} context + * @param {boolean=} stop + */ +async function getFromCache(type, cache, source, context, stop) { + const childNodes = await getChildNodes(cache, source.id, source.internal.contentDigest); + // Hack alert: ensure plugin has been run by querying htmlAst, + // which is set via `setFieldsOnGraphQLNodeType` by gatsby-transformer-remark, + // therefore might not have been run before this resolver runs. + if (!childNodes && !stop) { + await context.nodeModel.runQuery({ + query: { + filter: { + id: { eq: source.id }, + htmlAst: { ne: null }, + }, + }, + type: 'MarkdownRemark', + firstOnly: true, + }); + return getFromCache(cache, source, context, true); + } + if (!childNodes) { + logger.error( + 'gatsby-remark-vscode couldn’t retrieve up-to-date GRVSCCodeBlock GraphQL nodes. ' + + 'The `GRVSCCodeBlocks` field may be missing, empty or stale. ' + + 'The Gatsby cache is probably in a weird state. Try running `gatsby clean`, and file an ' + + 'issue at https://github.com/andrewbranch/gatsby-remark-vscode/issues/new if the problem persists.' + ); + + return context.nodeModel.runQuery({ + query: { parent: { id: { eq: source.id } } }, + type, + firstOnly: false + }); + } + return childNodes || []; +} From 9407dac2699842402fb4d761727fc4dda9464b86 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Sat, 2 May 2020 19:47:30 -0700 Subject: [PATCH 4/5] Add tests --- src/createCodeNodeRegistry.js | 3 +- src/graphql/getCodeSpanDataFromRegistry.js | 8 ++- .../baselines/inline-custom-class.html | 51 +++++++++++++++ .../baselines/inline-theme-function.html | 64 +++++++++++++++++++ test/integration/baselines/inline.html | 51 +++++++++++++++ .../cases/inline-custom-class/options.js | 7 ++ .../cases/inline-custom-class/test.md | 9 +++ .../cases/inline-theme-function/options.js | 15 +++++ .../cases/inline-theme-function/test.md | 9 +++ test/integration/cases/inline/options.js | 6 ++ test/integration/cases/inline/test.md | 9 +++ 11 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 test/integration/baselines/inline-custom-class.html create mode 100644 test/integration/baselines/inline-theme-function.html create mode 100644 test/integration/baselines/inline.html create mode 100644 test/integration/cases/inline-custom-class/options.js create mode 100644 test/integration/cases/inline-custom-class/test.md create mode 100644 test/integration/cases/inline-theme-function/options.js create mode 100644 test/integration/cases/inline-theme-function/test.md create mode 100644 test/integration/cases/inline/options.js create mode 100644 test/integration/cases/inline/test.md diff --git a/src/createCodeNodeRegistry.js b/src/createCodeNodeRegistry.js index c514f1b..34310f6 100644 --- a/src/createCodeNodeRegistry.js +++ b/src/createCodeNodeRegistry.js @@ -35,7 +35,8 @@ function createCodeNodeRegistry({ prefixAllClassNames } = {}) { forEachLine: (node, action) => blockMap.get(node).lines.forEach(action), forEachToken: (node, lineIndex, tokenAction) => { generateClassNames(); - const { tokenizationResults, isTokenized, lines } = blockMap.get(node); + const map = node.type === 'code' ? blockMap : spanMap; + const { tokenizationResults, isTokenized, lines } = map.get(node); if (!isTokenized) { return; } diff --git a/src/graphql/getCodeSpanDataFromRegistry.js b/src/graphql/getCodeSpanDataFromRegistry.js index 9ddf2fb..ab0c6e1 100644 --- a/src/graphql/getCodeSpanDataFromRegistry.js +++ b/src/graphql/getCodeSpanDataFromRegistry.js @@ -1,6 +1,8 @@ const { renderHTML } = require('../renderers/html'); -const { partitionOne } = require('../utils'); +const { partitionOne, flatMap } = require('../utils'); const { createTokenElement, createCodeSpanElement } = require('../factory/html'); +const { getThemeClassNames } = require('../themeUtils'); +const { joinClassNames } = require('../renderers/css'); /** * @template {Keyable} TKey @@ -27,7 +29,9 @@ function getCodeSpanDataFromRegistry(registry, key, codeSpan, getClassName) { }); }); - const className = getClassName(); + const customClassName = getClassName(); + const themeClassNames = flatMap(possibleThemes, getThemeClassNames); + const className = joinClassNames(customClassName, ...themeClassNames); const html = createCodeSpanElement(index, languageName, className, tokenElements); const [defaultTheme, additionalThemes] = partitionOne(possibleThemes, t => t.conditions.some(c => c.condition === 'default') diff --git a/test/integration/baselines/inline-custom-class.html b/test/integration/baselines/inline-custom-class.html new file mode 100644 index 0000000..9089046 --- /dev/null +++ b/test/integration/baselines/inline-custom-class.html @@ -0,0 +1,51 @@ +

Inline Code

+

Lorem ipsum, and .grvsc-container { display: block }.

+

(x: string, y: boolean) => {}

+

leading space should be preserved

+

`This should be fine ${because} the Markdown parser should have already trimmed the leading space.`

+ \ No newline at end of file diff --git a/test/integration/baselines/inline-theme-function.html b/test/integration/baselines/inline-theme-function.html new file mode 100644 index 0000000..90de941 --- /dev/null +++ b/test/integration/baselines/inline-theme-function.html @@ -0,0 +1,64 @@ +

Inline Code

+

Lorem ipsum, and .grvsc-container { display: block }.

+

(x: string, y: boolean) => {}

+

leading space should be preserved

+

`This should be fine ${because} the Markdown parser should have already trimmed the leading space.`

+ \ No newline at end of file diff --git a/test/integration/baselines/inline.html b/test/integration/baselines/inline.html new file mode 100644 index 0000000..afb7d3b --- /dev/null +++ b/test/integration/baselines/inline.html @@ -0,0 +1,51 @@ +

Inline Code

+

Lorem ipsum, and .grvsc-container { display: block }.

+

(x: string, y: boolean) => {}

+

leading space should be preserved

+

`This should be fine ${because} the Markdown parser should have already trimmed the leading space.`

+ \ No newline at end of file diff --git a/test/integration/cases/inline-custom-class/options.js b/test/integration/cases/inline-custom-class/options.js new file mode 100644 index 0000000..6641feb --- /dev/null +++ b/test/integration/cases/inline-custom-class/options.js @@ -0,0 +1,7 @@ +module.exports = { + inlineCode: { + theme: 'Default Light+', + marker: '•', + className: 'my-inline-code' + } +}; diff --git a/test/integration/cases/inline-custom-class/test.md b/test/integration/cases/inline-custom-class/test.md new file mode 100644 index 0000000..32afe53 --- /dev/null +++ b/test/integration/cases/inline-custom-class/test.md @@ -0,0 +1,9 @@ +# Inline Code + +Lorem `ipsum`, and `css•.grvsc-container { display: block }`. + +``ts•(x: string, y: boolean) => {}`` + +`sh• leading space should be preserved` + +`` js•`This should be fine ${because} the Markdown parser should have already trimmed the leading space.` `` \ No newline at end of file diff --git a/test/integration/cases/inline-theme-function/options.js b/test/integration/cases/inline-theme-function/options.js new file mode 100644 index 0000000..135714e --- /dev/null +++ b/test/integration/cases/inline-theme-function/options.js @@ -0,0 +1,15 @@ +module.exports = { + inlineCode: { + theme: ({ language }) => { + if (language === 'js') { + return { + default: 'Default Light+', + dark: 'Default Dark+' + }; + } + return 'Abyss'; + }, + marker: '•', + className: 'my-inline-code' + } +}; diff --git a/test/integration/cases/inline-theme-function/test.md b/test/integration/cases/inline-theme-function/test.md new file mode 100644 index 0000000..32afe53 --- /dev/null +++ b/test/integration/cases/inline-theme-function/test.md @@ -0,0 +1,9 @@ +# Inline Code + +Lorem `ipsum`, and `css•.grvsc-container { display: block }`. + +``ts•(x: string, y: boolean) => {}`` + +`sh• leading space should be preserved` + +`` js•`This should be fine ${because} the Markdown parser should have already trimmed the leading space.` `` \ No newline at end of file diff --git a/test/integration/cases/inline/options.js b/test/integration/cases/inline/options.js new file mode 100644 index 0000000..594e34d --- /dev/null +++ b/test/integration/cases/inline/options.js @@ -0,0 +1,6 @@ +module.exports = { + inlineCode: { + theme: 'Default Light+', + marker: '•' + } +}; diff --git a/test/integration/cases/inline/test.md b/test/integration/cases/inline/test.md new file mode 100644 index 0000000..32afe53 --- /dev/null +++ b/test/integration/cases/inline/test.md @@ -0,0 +1,9 @@ +# Inline Code + +Lorem `ipsum`, and `css•.grvsc-container { display: block }`. + +``ts•(x: string, y: boolean) => {}`` + +`sh• leading space should be preserved` + +`` js•`This should be fine ${because} the Markdown parser should have already trimmed the leading space.` `` \ No newline at end of file From 87813b1c8fd7487ba1b4778b1f77f3fe11dfe57a Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Sun, 3 May 2020 19:31:32 -0700 Subject: [PATCH 5/5] Add docs --- README.md | 146 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 131 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8cbe442..4ddd607 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,11 @@ If you’re updating from v1.x.x to v2.x.x, see [MIGRATING.md](./MIGRATING.md). - [Variables](#variables) - [Tweaking or replacing theme colors](#tweaking-or-replacing-theme-colors) - [Extra stuff](#extra-stuff) + - [Inline code highlighting](#inline-code-highlighting) - [Line highlighting](#line-highlighting) - [Using different themes for different code fences](#using-different-themes-for-different-code-fences) - [Arbitrary code fence options](#arbitrary-code-fence-options) +- [Options reference](#options-reference) - [Contributing](#contributing) ## Why gatsby-remark-vscode? @@ -48,7 +50,7 @@ Install the package: npm install --save gatsby-remark-vscode ``` -Add to your `gatsby-config.js` (all options are optional; defaults shown here): +Add to your `gatsby-config.js`: ```js { @@ -58,21 +60,8 @@ Add to your `gatsby-config.js` (all options are optional; defaults shown here): options: { plugins: [{ resolve: `gatsby-remark-vscode`, - // All options are optional. Defaults shown here. options: { - theme: 'Dark+ (default dark)', // Read on for list of included themes. Also accepts object and function forms. - wrapperClassName: '', // Additional class put on 'pre' tag. Also accepts function to set the class dynamically. - injectStyles: true, // Injects (minimal) additional CSS for layout and scrolling - extensions: [], // Third-party extensions providing additional themes and languages - languageAliases: {}, // Map of custom/unknown language codes to standard/known language codes - replaceColor: x => x, // Function allowing replacement of a theme color with another. Useful for replacing hex colors with CSS variables. - getLineClassName: ({ // Function allowing dynamic setting of additional class names on individual lines - content, // - the string content of the line - index, // - the zero-based index of the line within the code fence - language, // - the language specified for the code fence - meta // - any options set on the code fence alongside the language (more on this later) - }) => '', - logLevel: 'warn' // Set to 'info' to debug if something looks wrong + theme: 'Abyss' // Or install your favorite theme from GitHub } }] } @@ -329,6 +318,41 @@ Since the CSS for token colors is auto-generated, it’s fragile and inconvenien ## Extra stuff +### Inline code highlighting + +To highlight inline code spans, add an `inlineCode` key to the plugin options and choose a `marker` string: + +```js +{ + inlineCode: { + marker: '•' + } +} +``` + +Then, in your Markdown, you can prefix code spans by the language name followed by the `marker` string to opt into highlighting that span: + +```md +Now you can highlight inline code: `js•Array.prototype.concat.apply([], array)`. +``` + +The syntax theme defaults to the one selected for code blocks, but you can control the inline code theme independently: + +```js +{ + theme: 'Default Dark+', + inlineCode: { + marker: '•', + theme: { + default: 'Default Light+', + dark: 'Default Dark+' + } + } +} +``` + +See [`inlineCode`](#inlinecode) in the options reference for more API details. + ### Line highlighting `gatsby-remark-vscode` offers the same line-range-after-language-name strategy of highlighting or emphasizing lines as [gatsby-remark-prismjs](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-remark-prismjs): @@ -416,6 +440,98 @@ Line numbers and ranges aren’t the only things you can pass as options on your } ``` +## Options reference + +### `theme` + +The syntax theme used for code blocks. + +- **Default:** `'Default Dark+'` +- **Accepted types**: + - **`string`:** The name or id of a theme. (See [Built-in themes](#themes) and [Using languages and themes from an extension](#using-languages-and-themes-from-an-extension).) + - **`ThemeSettings`:** An object that selects different themes to use in different contexts. (See [Multi-theme support](#multi-theme-support).) + - **`(data: CodeBlockData) => string | ThemeSettings`:** A function returning the theme selection for a given code block. `CodeBlockData` is an object with properties: + - **`language`:** The language of the code block, if one was specified. + - **`markdownNode`:** The MarkdownRemark GraphQL node. + - **`node`:** The Remark AST node of the code block. + - **`parsedOptions`:** The object form of of any code fence info supplied. (See [Arbitrary code fence options](#arbitrary-code-fence-options).) + +### `wrapperClassName` + +A custom class name to be set on the `pre` tag. + +- **Default:** None, but the class `grvsc-container` will always be on the tag. +- **Accepted types:** + - **`string`:** The class name to add. + - **`(data: CodeBlockData) => string`:** A function returning the class name to add for a given code block. (See the [`theme`](#theme) option above for the details of `CodeBlockData`.) + +### `languageAliases` + +An object that allows additional language names to be mapped to recognized languages so they can be used on opening code fences: + +```js +{ + languageAliases: { + fish: 'sh' + } +} +``` + +````md +Then you can use code fences like this: + +```fish +ls -la +``` + +And they’ll be parsed as shell script (`sh`). +```` + +- **Default:** None, but many built-in languages are already recognized by a variety of names. +- **Accepted type:** `Record`; that is, an object with string keys and string values. + +### `extensions` + +A list of third party extensions to search for additional langauges and themes. (See [Using languages and themes from an extension](#using-languages-and-themes-from-an-extension).) + +- **Default:** None +- **Accepted type:** `string[]`; that is, an array of strings, where the strings are the package names of the extensions. + +### `inlineCode` + +Enables syntax highlighting for inline code spans. (See [Inline code highlighting](#inline-code-highlighting).) + +- **Default:** None +- **Accepted type:** An object with properties: + - **`theme`:** A string or `ThemeSettings` object selecting the theme, or a function returning a string or `ThemeSettings` object for a given code span. The type is the same as the one documented in the top-level [theme option](#theme). Defaults to the value of the top-level [theme option](#theme). + - **`marker`:** A string used as a separator between the language name and the content of a code span. For example, with a `marker` of value `'•'`, you can highlight a code span as JavaScript by writing the Markdown code span as `` `js•Code.to.highlight("inline")` ``. + - **`className`:** A string, or function returning a string for a given code span, that sets a custom class name on the wrapper `code` HTML tag. If the function form is used, it is passed an object parameter describing the code span with properties: + - **`language`:** The language of the code span (the bit before the `marker` character). + - **`markdownNode`:** The MarkdownRemark GraphQL node. + - **`node`:** The Remark AST node of the code span. + +### `injectStyles` + +Whether to add supporting CSS to the end of the Markdown document. (See [Styles](#styles).) + +- **Default:** `true` +- **Accepted type:** `boolean` + +### `replaceColor` + +A function allowing individual color values to be replaced in the generated CSS. (See [Tweaking or replacing theme colors](#tweaking-or-replacing-theme-colors).) + +- **Default:** None +- **Accepted type:** `(colorValue: string, theme: string) => string`; that is, a function that takes the original color and the identifier of the theme it came from and returns a new color value. + +### `logLevel` + +The verbosity of logging. Useful for diagnosing unexpected behavior. + +- **Default**: `'warn'` +- **Accepted values:** From most verbose to least verbose, `'trace'`, `'debug'`, `'info'`, `'warn'`, or `'error'`. + + ## Contributing Please note that this project is released with a Contributor [Code of Conduct](./CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.