UNPKG

@yankeeinlondon/code-builder

Version:

Adds code highlighting support to vite-plugin-md

444 lines (439 loc) 15.4 kB
import { Fragment } from '@yankeeinlondon/happy-wrapper'; import { Grammar } from 'prismjs'; import * as _yankeeinlondon_builder_api from '@yankeeinlondon/builder-api'; import { Pipeline, PipelineStage } from '@yankeeinlondon/builder-api'; type HSL = `hsl(${number}${string}, ${number}${string}, ${number}${string})`; type HSLA = `hsla(${number}${string}, ${number}${string}, ${number}${string}, ${number}${string})`; type RGB = `rgb(${number}${string}, ${number}${string}, ${number}${string})`; type RGBA = `hsla(${number}${string}, ${number}${string}, ${number}${string}, ${number}${string})`; type HEX = `#${string}`; type Opacity = `/${number}` | ""; type Color = `${HSL | HSLA | RGB | RGBA | HEX | ""}${Opacity}`; type ColorByMode = [light: Color, dark: Color]; /** * Coloration/Theming of a code block */ interface CodeColorTheme<T extends Color | ColorByMode> { foreground: T; background: T; lineNumber: T; lineNumberGutter: T; highlight: T; /** the background color for text selection */ textSelection: T; /** * used in some grammars like SCSS where you might have `@apply` * but will same color as */ atrule?: T; keyword: T; attribute?: T; deleted?: T; inserted?: T; url?: T; function?: T; functionName?: T; class?: T; className?: T; builtin: T; tag?: T; comment: T; blockComment?: T; doctype?: T; cdata?: T; prolog?: T; pseudoElement?: T; pseudoClass?: T; property?: T; /** will use same as "property" if not expressed separately */ constant?: T; /** will use same as "property" if not expressed separately */ variable?: T; /** string values */ string: T; /** literal value; will use string if not specified */ literal?: T; /** uses the same as "string" by default */ char?: T; /** a boolean value; uses `builtin` if not specified */ boolean?: T; /** uses `builtin` if not specified */ selector?: T; placeholder?: T; /** uses `builtin` if not specified */ important?: T; /** uses `builtin` if not specified */ delimiter?: T; /** any language symbol not otherwise matched, will use keyword if not stated */ symbol?: T; /** will use "symbol" if not defined */ entity?: T; /** numeric values; will use symbol if not expressed separately */ number?: T; namespace?: T; /** * A form of recognized punctuation in the code */ punctuation?: T; punctuationFirstChild?: T; /** will use same as "punctuation" if not expressed separately */ operator?: T; /** will use same as "punctuation" if not expressed separately */ attrName?: T; attrValue?: T; attrValueForPunctuation?: T; attrValueForPunctuationFirstChild?: T; decorator?: T; regex: T; hexcode?: T; unit?: T; id?: T; jsonProperty?: T; /** .language-markup .token.tag */ markupTag?: T; /** .language-markup .token.attr-name */ markupAttrName?: T; /** .language-markup .token.punctuation */ markupPunctuation?: T; headingText?: T; footerText?: T; lineHighlightBackground?: T; /** * the text color for a highlighted line * ```css * .line-highlight.line-highlight:before, * .line-highlight.line-highlight[data-end]:after {} * ``` */ lineHighlightBeforeAfter?: T; /** the background color for a highlighted line */ lineHighlightBackgroundBeforeAfter?: T; } /** * A callback called for each line of a code block and responsible * for stating any _additional_ classes to add for this given line */ type LineCallback = ( /** the code, _only_ for the given line */ line: string, /** the full code block */ code: string, /** the language being converted to */ lang: string) => string; /** * Modifiers are single character tokens which are allowed * to precede the "language" in a fence statement to provide * some binary instruction to how to handle the code block. */ declare enum Modifier { /** * the pound symbol is used to indicate that line numbers * for a code block should _always_ be used regardless of * the option set */ "#" = "#", /** * using an asterisk modifier will force the line numbers * of a code block to NOT be used regardless of configuration */ "*" = "*", /** * the exclamation modifier indicates that a code block should * _toggle_ the normal configuration for escape code interpolation */ "!" = "!" } type CodeParsingStage = "code" | "dom" | "complete"; interface HighlightLine { kind: "line"; line: number; } interface HighlightRange { kind: "range"; from: number; to: number; } interface HighlightSymbol { kind: "symbol"; symbol: string; } /** * Tokens representing intent to highlight code; originating from any source * [highlight prop, csv, VuePress/Vitepress syntax] */ type HighlightToken = HighlightLine | HighlightRange | HighlightSymbol; type RangeExpression = `${string}-${string}`; /** the ways in which the user might express the "highlight" property */ type HighlightExpression = number | RangeExpression | { symbol: string; } | { from: number; to: number; }; interface CodeBlockProperties { /** * Indicates what lines to highlight, this can be: * - a single line number * - a line range * - a line with a give token block (TODO: figure out how to model) */ highlight?: HighlightExpression | HighlightExpression[]; /** * Allows pointing to an external file as the code source */ filename?: string; /** classes to add to the heading section */ heading?: string; /** classes to add to the footer section */ footer?: string; /** classes to add to the codeblock section */ class?: string; /** style properties to add to the codeblock section */ style?: string; width?: number | string; height?: number | string; alt?: string; tooltip?: any; "data-codeLines"?: number; [key: string]: any; } type CodeFilename = boolean | "filename" | "with-path" | `name:${string}`; /** * When a fence block is encountered it will be parsed * into the following structure for evaluation. */ interface CodeBlockMeta<S extends CodeParsingStage> { /** * The finalized HTML based on the code pipeline */ html: S extends "complete" ? string : never; /** the <pre> block wrapper */ pre: S extends "code" ? string : Fragment; lineNumbersWrapper: S extends "code" ? string : Fragment; codeBlockWrapper: S extends "code" ? string : Fragment; /** * All highlighting information will be captured as * an array of `HighlightToken` tokens. */ highlightTokens: HighlightToken[]; /** * The external filename which the code block is importing (`null` if code is * not external). */ externalFile: string | null; /** * Specifies whether the filename should be displayed above the code block. * With _external code references_ the boolean turns this on/off and when * "on" it displays just the filename. You may also be more explicit by using * the `filename` or `with-path` string literals. * * Finally, should you want to explicitly state a filename -- useful for * non-external code -- you can put in a string value prefixed with `name:`. * If you are using an external file, this will override the actual file name. */ showFilename: CodeFilename; /** * Typically _not_ used but when a code block references an external file * AND the local code block _also_ has code, then it will be placed here */ aboveTheFoldCode?: S extends "code" ? string : Fragment; /** * The code block; represented as either a string or a DOM tree * based on lifecycle */ code: S extends "code" ? string : Fragment; /** * An optional heading to put above the code block */ heading?: S extends "code" ? string : Fragment; /** * An optional footer to put under the code block */ footer?: S extends "code" ? string : Fragment; /** * The number of lines in the code block */ codeLinesCount: S extends "code" ? never : number; /** * The tagName for the block; will be `code` except for edge cases */ tag: string; /** * The nesting level */ level: number; /** * The language used by the highlighter */ lang: string; /** The originally requested language by user */ requestedLang: string; /** * An optional message that a pipeline function can export and will * be picked up by trace() utility */ trace?: string; /** * The properties found on the top line (to right of language and back ticks), these * key/value pairs will be assigned to the */ props: CodeBlockProperties; modifiers: Modifier[]; /** * not sure how useful this is yet ... currently always evaluates to three * back ticks */ markup: string; } /** * A callback for a block node which provides build-time capability to * modify a property with a callback */ type BlockCallback<T> = <S extends CodeParsingStage>(fence: CodeBlockMeta<S>, filename: string, frontmatter: Pipeline<PipelineStage.parser>["frontmatter"]) => T; /** * Options for the `code-builder` extension */ interface CodeOptions { /** * The language to use for code blocks that specify a language that Prism does not know. * * @default 'plain' */ defaultLanguageForUnknown?: string; /** * The language to use for code blocks that do not specify a language. * * @default 'plain' */ defaultLanguageForUnspecified: string; /** * Shorthand to set both {@code defaultLanguageForUnknown} and {@code defaultLanguageForUnspecified} to the same value. Will be copied * to each option if it is set to {@code undefined}. */ defaultLanguage?: string; /** * Hook into the fence mutation process _before_ the builder * gets involved. */ before: (fence: CodeBlockMeta<"code">, payload: Pipeline<PipelineStage.parser>, options: CodeOptions) => CodeBlockMeta<"code">; /** * Hook into the fence mutation process _after_ the builder * has mutated CodeFenceMeta to it's configured rules. */ after: (fence: CodeBlockMeta<"dom">, payload: Pipeline<PipelineStage.parser>, options: CodeOptions) => CodeBlockMeta<"dom">; /** * By default each _line_ in the code will be given a class of "line" but you can override this * default behavior in one of the following ways: * * 1. if for some reason you want to _change_ the class name you may pass in a static string value * which will be used instead of "line". * 2. if you pass in a `LineCallback` function you will receive the code on that line along with the language and you can opt to _add_ additional classes (the "line" class will persist regardless of what you return) * 3. if you want _no classes_ then you can pass in a `false` value to indicate this */ lineClass?: string | false | LineCallback; /** * The vuepress/vitepress implementation of code blocks appears to use an interesting * DOM structure to bring in line numbers that didn't feel naturally intuitive (but may * have been done for very good reason). If you wish to use this structure you can configure * to use the `flex-lines` style. * * By default we use the 'tabular' layout which feels more intuitive and has full testing * behind it (in this repo). * * @default 'tabular' */ layoutStructure: "flex-lines" | "tabular"; /** * Any default classes to add to the header region (when region is found to exist); * if you override this be aware that some styling may expect the default "heading" class * to exist. * * @default 'heading' */ headingClasses?: string[] | BlockCallback<string[]>; /** * Any default classes to add to the footer region (when region is found to exist); * if you override this be aware that some styling may expect the default "footer" class * to exist. * * @default 'footer' */ footerClasses?: string[] | BlockCallback<string[]>; /** * Allows to turn on/off the feature of highlighting lines in code; this is just a "default" * as individual code blocks can explicitly ask for line numbers with the `#` modifier * * @default false */ lineNumbers: boolean; /** * Flag indicating whether to display the language name in the upper right * of the code block. * * @default true */ showLanguage: boolean | BlockCallback<boolean>; /** * Allows to turn on/off the feature of _highlighting_ lines in code; lines will never * be highlighted unless the page has instructions to highlight particular lines but * this allows all highlights to be explicitly turned off * * @default true */ highlightLines: boolean | BlockCallback<boolean>; /** * Adds a clipboard icon to the header row and injects the functionality to * copy code block contents to the clipboard. * * @default false */ clipboard: boolean | BlockCallback<boolean>; /** * The `copyToClipboard()` and `clipboardAvailable()` functions are automatically * added to pages which have a code block on the page which requests this functionality * but you can also just ask for it to be included in call pages so you can use these * functions for your own evil plans. */ provideClipboardFunctionality: boolean | BlockCallback<boolean>; theme?: "base" | "solarizedLight" | "material" | "dracula" | "tomorrow" | "duotone" | CodeColorTheme<any>; /** * By default light mode has code blocks with light backgrounds, and dark mode with * dark backgrounds. If you want to _invert_ that you can by setting this property to * true. * * @default false */ invertColorMode?: boolean; /** * Should you want to add your own language grammar you can: * [Extending Prism Language Definitions](https://prismjs.com/extending.html#language-definitions) */ languageGrammars?: Record<string, Grammar>; /** * Allows stating whether you want meta-data about the code block injected * into the Frontmatter. When resolving to a `true` the code block will be added * to the Frontmatter property "_codeBlocks". * * @default false */ injectIntoFrontmatter?: boolean | BlockCallback<boolean>; /** * Code blocks will default to the following inline style: * ```css * .code-block { * font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; * } * ``` * * but you can override this inline style if you wish. Obviously you can also just * target the `.code-block` class to whatever you like */ codeFont?: string; } /** * `code` Builder API * * Provides highlighting of code features to `vite-plugin-md` */ declare const plugin: _yankeeinlondon_builder_api.BuilderApi<Partial<CodeOptions>, "parser">; export { CodeBlockMeta, CodeOptions, CodeParsingStage, plugin as default };