├── .cz.json ├── .gitignore ├── LICENSE ├── README.md ├── components ├── CodeBlock.astro └── index.ts ├── env.d.ts ├── package.json ├── pnpm-lock.yaml ├── src ├── index.ts ├── utils │ ├── color-contrast.ts │ ├── copy-button.ts │ ├── html-entities.ts │ ├── makeComponentNode.ts │ ├── shiki-block.ts │ ├── shiki-line.ts │ ├── syntax-highlighting-theme.ts │ ├── types.ts │ └── user-config.ts └── virtual.d.ts └── tsconfig.json /.cz.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitizen": { 3 | "name": "cz_conventional_commits", 4 | "tag_format": "v$version", 5 | "version_type": "semver", 6 | "version_provider": "npm", 7 | "major_version_zero": true 8 | } 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 The Web Forge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # astro-code-blocks 2 | 3 | Beautiful code blocks for your Astro site 4 | This is a custom version of the integration from Astro Docs 5 | 6 | ## Quick Install 7 | 8 | The astro add command-line tool automates the installation for you. Run one of the following commands in a new terminal window. (If you aren’t sure which package manager you’re using, run the first command.) Then, follow the prompts, and type “y” in the terminal (meaning “yes”) for each one. 9 | 10 | ```sh 11 | # Using NPM 12 | npx astro add @thewebforge/astro-code-blocks 13 | # Using Yarn 14 | yarn astro add @thewebforge/astro-code-blocks 15 | # Using PNPM 16 | pnpm astro add @thewebforge/astro-code-blocks 17 | ``` 18 | 19 | If you run into any issues, feel free to report them to us on GitHub and try the manual installation steps below. 20 | 21 | ## Manual Install 22 | 23 | First, install the @thewebforge/astro-code-blocks package using your package manager. If you’re using npm or aren’t sure, run this in the terminal: 24 | 25 | ```sh 26 | npm install @thewebforge/astro-code-blocks 27 | ``` 28 | 29 | If you’re using Yarn or PNPM, run this instead: 30 | 31 | ```sh 32 | # Using Yarn 33 | yarn add @thewebforge/astro-code-blocks 34 | # Using PNPM 35 | pnpm add @thewebforge/astro-code-blocks 36 | ``` 37 | 38 | Next, open your project’s astro.config.mjs file and add the following to the plugins array: 39 | 40 | **astro.config.mjs** 41 | 42 | ```js 43 | import codeblocks from "@thewebforge/astro-code-blocks"; 44 | 45 | export default defineConfig({ 46 | integrations: [ 47 | codeblocks(), 48 | ], 49 | }); 50 | ``` 51 | > **Warning** 52 | > If you alreday installed Astro MDX integration. Or another integration that uses MDX, you need to make sure that the codeblocks integration comes before MDX in the integrations array. Otherwise, it will not work. 53 | 54 | ### Config 55 | 56 | You can configure the code blocks integration by passing an object to the codeblocks function. 57 | 58 | **astro.config.mjs** 59 | ```js 60 | integrations: [ 61 | codeblocks({ 62 | // Copy Button Options 63 | copyButtonTitle: 'Copy', 64 | copyButtonTooltip: 'Copied to clipboard', 65 | }), 66 | ], 67 | ``` 68 | 69 | ## Usage 70 | 71 | With the integration installed, the component will automatically be imported and applied ot the code blocks you create in your `.mdx` files. 72 | 73 | ### Add a title to your code block 74 | 75 | You can add a title to your code block by adding a `title` prop to the code block 76 | 77 | ````mdx 78 | ```js title="myscript.js" 79 | console.log('Hello World') 80 | ``` 81 | ```` 82 | > **Note** 83 | > The title prop is optional. If you don't add it, the code block will not have a title. 84 | 85 | ### Highlight lines 86 | 87 | You can highlight lines in your code block by adding a prop to the code blocks as a list of 88 | comma separated numbers in curly brackets. For example: 89 | - `{1}` will highlight line 1 90 | - `{1,3}` will highlight lines 1 and 3 91 | - `{2-5, 7}` will highlight lines 1 to 5(not included) and 7 92 | 93 | ````mdx 94 | ```js {1,3,5} 95 | console.log('Hello World') 96 | console.log('Hello World') 97 | console.log('Hello World') 98 | console.log('Hello World') 99 | console.log('Hello World') 100 | ``` 101 | ```` 102 | 103 | ### Highlight strings 104 | 105 | You can highlight strings in your code block by adding a prop to the code blocks as a regular expression. The following example will highlight all occurences of "astro": 106 | 107 | - `/astro/` will highlight all occurences of "astro" 108 | - `/\w*$/` will highlight the last word of each line 109 | 110 | ````mdx 111 | ```sh /astro/ 112 | # Using NPM 113 | npx astro add @thewebforge/astro-code-blocks 114 | # Using Yarn 115 | yarn astro add @thewebforge/astro-code-blocks 116 | # Using PNPM 117 | pnpm astro add @thewebforge/astro-code-blocks 118 | ``` 119 | ```` 120 | 121 | ### Insertions and Deletions 122 | 123 | You can highlight insertions and deletions in your code block by adding `ins` and/or `del` props to the code blocks as a list of lines in curly brackets. For example: 124 | 125 | ````mdx 126 | ```sh ins={3,4} del={5,6} 127 | # Using NPM 128 | npx astro add @thewebforge/astro-code-blocks 129 | # Using Yarn 130 | yarn astro add @thewebforge/astro-code-blocks 131 | # Using PNPM 132 | pnpm astro add @thewebforge/astro-code-blocks 133 | ``` 134 | ```` 135 | 136 | ## Custom CSS 137 | 138 | ### Override the default Astro styles 139 | 140 | __`crazy-code-props.css`__ 141 | ```css 142 | :root { 143 | --astro-code-color-text: white; 144 | --astro-code-color-background: black; 145 | --astro-code-token-constant: plum; 146 | --astro-code-token-string: purple; 147 | --astro-code-token-comment: tomato; 148 | --astro-code-token-keyword: darkslategrey; 149 | --astro-code-token-parameter: coral; 150 | --astro-code-token-function: green; 151 | --astro-code-token-string-expression: chartreuse; 152 | --astro-code-token-punctuation: gray; 153 | --astro-code-token-link: firebrick; 154 | } 155 | ``` 156 | -------------------------------------------------------------------------------- /components/CodeBlock.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // @ts-ignore 3 | import config from 'virtual:codeblocks/user-config'; 4 | import { ShikiBlock } from '../dist/utils/shiki-block.js'; 5 | import { 6 | InlineMarkingDefinition, 7 | LineMarkingDefinition, 8 | MarkerType, 9 | MarkerTypeOrder, 10 | } from '../dist/utils/types.js'; 11 | 12 | export type Props = { 13 | lang?: string; 14 | title?: string; 15 | removedLineIndex?: string; 16 | removedLineCount?: string; 17 | lineMarkings?: string; 18 | inlineMarkings?: string; 19 | } 20 | 21 | const copyButtonTitle: string = config.copyButtonTitle ?? 'Copy'; 22 | const copyButtonTooltip: string = config.copyButtonTooltip ?? 'Copied!'; 23 | 24 | const { 25 | lang = '', 26 | removedLineIndex = '', 27 | removedLineCount = '', 28 | title = '', 29 | lineMarkings = '', 30 | inlineMarkings = '', 31 | } = Astro.props as Props; 32 | 33 | const isTerminal = ['shellscript', 'shell', 'bash', 'sh', 'zsh'].includes(lang); 34 | const intRemovedLineIndex = parseInt(removedLineIndex) || 0; 35 | const intRemovedLineCount = parseInt(removedLineCount) || 0; 36 | 37 | // Generate HTML code from the title (if any), improving the ability to wrap long file paths 38 | // into multiple lines by inserting a line break opportunity after each slash 39 | const titleHtml = decodeURIComponent(title).replace(/([\\/])/g, '$1'); 40 | 41 | // Render the default slot, which contains the syntax-highlighted code in HTML format 42 | // as processed by Astro's Shiki integration 43 | 44 | let codeSnippetHtml = await Astro.slots.render('default'); 45 | 46 | // Mark lines and expressions (if requested) 47 | codeSnippetHtml = applyMarkings(codeSnippetHtml, lineMarkings, inlineMarkings); 48 | 49 | function rangeParser(string: string) { 50 | let res: number[] = []; 51 | let m: RegExpMatchArray | null; 52 | 53 | for (let str of string.split(",").map((str) => str.trim())) { 54 | // just a number 55 | if (/^-?\d+$/.test(str)) { 56 | res.push(parseInt(str, 10)); 57 | } else if ( 58 | (m = str.match(/^(-?\d+)(-|\.\.\.?|\u2025|\u2026|\u22EF)(-?\d+)$/)) 59 | ) { 60 | // 1-5 or 1..5 (equivalent) or 1...5 (doesn't include 5) 61 | let [_, lhs, sep, rhs] = m; 62 | 63 | if (lhs && rhs) { 64 | let lhsNum = parseInt(lhs); 65 | let rhsNum = parseInt(rhs); 66 | const incr = lhsNum < rhsNum ? 1 : -1; 67 | 68 | // Make it inclusive by moving the right 'stop-point' away by one. 69 | if (sep === "-" || sep === ".." || sep === "\u2025") rhs += incr; 70 | 71 | for (let i = lhsNum; i !== rhsNum; i += incr) res.push(i); 72 | } 73 | } 74 | } 75 | 76 | return res; 77 | } 78 | function applyMarkings( 79 | highlightedCodeHtml: string, 80 | strLineMarkings: string, 81 | strInlineMarkings: string 82 | ) { 83 | const lineMarkings: LineMarkingDefinition[] = parseMarkingDefinition( 84 | strLineMarkings, 85 | // Syntax: [mark=|del=|ins=]{2-5,7} 86 | /^(?:(.*)=){(.+)}$/, 87 | `Invalid code snippet line marking: Expected a range like "{2-5,7}", 88 | optionally with one of the prefixes "mark=", "del=" or "ins=", but got "$entry"` 89 | ).map(({ markerType, groupValues: [content] }) => { 90 | const lines = rangeParser(content); 91 | 92 | // If any lines were removed during preprocessing, 93 | // automatically shift marked line numbers accordingly 94 | const shiftedLines = lines.map((lineNum) => { 95 | if (lineNum <= intRemovedLineIndex) return lineNum; 96 | if (lineNum > intRemovedLineIndex + intRemovedLineCount) return lineNum - intRemovedLineCount; 97 | return -1; 98 | }); 99 | 100 | return { 101 | markerType, 102 | lines: shiftedLines, 103 | }; 104 | }); 105 | 106 | const inlineMarkings: InlineMarkingDefinition[] = parseMarkingDefinition( 107 | strInlineMarkings, 108 | // Syntax for plaintext strings: 109 | // - Double quotes: [mark=|del=|ins=]" 28 |

${this.tooltip}

29 | `; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/html-entities.ts: -------------------------------------------------------------------------------- 1 | import { unescape as unEsc } from "html-escaper"; 2 | export { escape } from "html-escaper"; 3 | 4 | /** Unescape HTML while catering for `<` (`<`) and `'&'` (`&`), which the Astro compiler outputs. */ 5 | export function unescape(str: string) { 6 | return unEsc(str).replaceAll("<", "<").replaceAll("&", "&"); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/makeComponentNode.ts: -------------------------------------------------------------------------------- 1 | import type { BlockContent } from "mdast"; 2 | import type { MdxJsxAttribute, MdxJsxFlowElement } from "mdast-util-mdx-jsx"; 3 | 4 | interface NodeProps { 5 | attributes?: Record; 6 | } 7 | 8 | /** 9 | * Create AST node for a custom component injection. 10 | * 11 | * @example 12 | * makeComponentNode('MyComponent', { prop: 'val' }, h('p', 'Paragraph inside component')) 13 | * 14 | */ 15 | export function makeComponentNode( 16 | name: string, 17 | { attributes = {} }: NodeProps = {}, 18 | ...children: BlockContent[] 19 | ): MdxJsxFlowElement { 20 | return { 21 | type: "mdxJsxFlowElement", 22 | name, 23 | attributes: Object.entries(attributes) 24 | // Filter out non-truthy attributes to avoid empty attrs being parsed as `true`. 25 | .filter(([_k, v]) => v !== false && Boolean(v)) 26 | .map(([name, value]) => ({ 27 | type: "mdxJsxAttribute", 28 | name, 29 | value: value as MdxJsxAttribute["value"], 30 | })), 31 | children, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/shiki-block.ts: -------------------------------------------------------------------------------- 1 | import { CopyButton } from "./copy-button"; 2 | import type { CopyButtonArgs } from "./copy-button"; 3 | import { ShikiLine } from "./shiki-line"; 4 | import { MarkerTypeOrder } from "./types"; 5 | import type { 6 | InlineMarkingDefinition, 7 | LineMarkingDefinition, 8 | } from "./types"; 9 | 10 | export class ShikiBlock { 11 | private htmlBeforeFirstLine = ""; 12 | private shikiLines: ShikiLine[] = []; 13 | private htmlAfterLastLine = ""; 14 | private copyButton: CopyButton | null = null; 15 | 16 | constructor(highlightedCodeHtml: string, copyButtonArgs: CopyButtonArgs) { 17 | if (!highlightedCodeHtml) return; 18 | 19 | const codeBlockRegExp = 20 | /^\s*()([\s\S]*)(<\/code><\/pre>)\s*$/; 21 | const matches = highlightedCodeHtml.match(codeBlockRegExp); 22 | if (!matches) 23 | throw new Error( 24 | `Shiki-highlighted code block HTML did not match expected format. HTML code:\n${highlightedCodeHtml}` 25 | ); 26 | 27 | this.htmlBeforeFirstLine = matches[1]; 28 | const innerHtml = matches[2]; 29 | this.htmlAfterLastLine = matches[3]; 30 | 31 | // Parse inner HTML code to ShikiLine instances 32 | const innerHtmlLines = innerHtml.split(/\r?\n/); 33 | 34 | this.shikiLines = innerHtmlLines.map((htmlLine) => new ShikiLine(htmlLine)); 35 | this.copyButton = new CopyButton(this.shikiLines, copyButtonArgs); 36 | } 37 | 38 | applyMarkings( 39 | lineMarkings: LineMarkingDefinition[], 40 | inlineMarkings: InlineMarkingDefinition[] 41 | ) { 42 | if (!lineMarkings.length && !inlineMarkings.length) return; 43 | this.shikiLines.forEach((line, i) => { 44 | // Determine line marker type (if any) 45 | const matchingDefinitions = lineMarkings.filter((def) => 46 | def.lines.includes(i + 1) 47 | ); 48 | if (matchingDefinitions) { 49 | const markerTypes = matchingDefinitions.map((def) => def.markerType); 50 | markerTypes.sort( 51 | (a, b) => MarkerTypeOrder.indexOf(a) - MarkerTypeOrder.indexOf(b) 52 | ); 53 | const highestPrioMarkerType = markerTypes[0]; 54 | line.setLineMarkerType(highestPrioMarkerType); 55 | } 56 | 57 | line.applyInlineMarkings(inlineMarkings); 58 | }); 59 | } 60 | 61 | renderToHtml() { 62 | const linesHtml = this.shikiLines 63 | .map((line) => { 64 | // line.ensureTokenColorContrast(); 65 | return line.renderToHtml(); 66 | }) 67 | .join("\n"); 68 | const copyButton = this.copyButton?.renderToHtml(); 69 | return `${this.htmlBeforeFirstLine}${linesHtml}${this.htmlAfterLastLine}${copyButton}`; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/shiki-line.ts: -------------------------------------------------------------------------------- 1 | import chroma from "chroma-js"; 2 | import { unescape } from "./html-entities"; 3 | import { ensureTextContrast } from "./color-contrast"; 4 | import { MarkerTypeOrder } from "./types"; 5 | import type { 6 | InlineMarkingDefinition, 7 | InlineToken, 8 | InsertionPoint, 9 | MarkedRange, 10 | MarkerToken, 11 | MarkerType, 12 | } from "./types"; 13 | 14 | export class ShikiLine { 15 | readonly tokens: InlineToken[]; 16 | readonly textLine: string; 17 | 18 | private beforeClassValue: string; 19 | private classes: Set; 20 | private afterClassValue: string; 21 | private afterTokens: string; 22 | 23 | constructor(highlightedCodeLine: string) { 24 | const lineRegExp = /^()(.*)(<\/span>)$/; 25 | const lineMatches = highlightedCodeLine.match(lineRegExp); 26 | if (!lineMatches) 27 | throw new Error( 28 | `Shiki-highlighted code line HTML did not match expected format. HTML code:\n${highlightedCodeLine}` 29 | ); 30 | 31 | this.beforeClassValue = lineMatches[1]; 32 | this.classes = new Set(lineMatches[2].split(" ")); 33 | this.afterClassValue = lineMatches[3]; 34 | const tokensHtml = lineMatches[4]; 35 | this.afterTokens = lineMatches[5]; 36 | 37 | // Split line into inline tokens, accomodate for hex or css variable colors 38 | const tokenRegExp = 39 | /(.*?)<\/span>/g; 40 | const tokenMatches = tokensHtml.matchAll(tokenRegExp); 41 | this.tokens = []; 42 | this.textLine = ""; 43 | for (const tokenMatch of tokenMatches) { 44 | const [, color, otherStyles, innerHtml] = tokenMatch; 45 | const text = unescape(innerHtml); 46 | this.tokens.push({ 47 | tokenType: "syntax", 48 | color, 49 | otherStyles, 50 | innerHtml, 51 | text, 52 | textStart: this.textLine.length, 53 | textEnd: this.textLine.length + text.length, 54 | }); 55 | this.textLine += text; 56 | } 57 | } 58 | 59 | applyInlineMarkings(inlineMarkings: InlineMarkingDefinition[]) { 60 | const markedRanges: MarkedRange[] = []; 61 | 62 | // Go through all definitions, find matches for their text or regExp in textLine, 63 | // and fill markedRanges with their capture groups or entire matches 64 | inlineMarkings.forEach((inlineMarking) => { 65 | const matches = this.getInlineMarkingDefinitionMatches(inlineMarking); 66 | markedRanges.push(...matches); 67 | }); 68 | 69 | if (!markedRanges.length) return; 70 | 71 | // Flatten marked ranges to prevent any overlaps 72 | const flattenedRanges = this.flattenMarkedRanges(markedRanges); 73 | if (!flattenedRanges.length) return; 74 | 75 | // Build an array of marker elements to insert 76 | const markerElements = flattenedRanges.map((range) => { 77 | return { 78 | markerType: range.markerType, 79 | opening: this.textPositionToTokenPosition(range.start), 80 | closing: this.textPositionToTokenPosition(range.end), 81 | }; 82 | }); 83 | 84 | // Mutate inline tokens in reverse direction (from end to start), 85 | // inserting opening and closing marker tokens at the determined positions, 86 | // optionally splitting syntax tokens if they only match partially 87 | markerElements.reverse().forEach((markerElement) => { 88 | const markerToken: MarkerToken = { 89 | tokenType: "marker", 90 | markerType: markerElement.markerType, 91 | }; 92 | 93 | this.insertMarkerTokenAtPosition(markerElement.closing, { 94 | ...markerToken, 95 | closing: true, 96 | }); 97 | this.insertMarkerTokenAtPosition(markerElement.opening, markerToken); 98 | }); 99 | } 100 | 101 | ensureTokenColorContrast() { 102 | // Ensure proper color contrast of syntax tokens inside marked ranges 103 | // (note that only the lightness of the background color is used) 104 | const backgroundColor = chroma("#2e336b"); 105 | const isLineMarked = this.getLineMarkerType() !== undefined; 106 | let inInlineMarker = false; 107 | this.tokens.forEach((token) => { 108 | if (token.tokenType === "marker") { 109 | inInlineMarker = !token.closing; 110 | return; 111 | } 112 | if (inInlineMarker || isLineMarked) { 113 | const tokenColor = chroma(token.color); 114 | const fixedTokenColor = ensureTextContrast(tokenColor, backgroundColor); 115 | token.color = fixedTokenColor.hex(); 116 | } 117 | }); 118 | } 119 | 120 | renderToHtml() { 121 | const classValue = [...this.classes].join(" "); 122 | 123 | // Build the line's inner HTML code by rendering all contained tokens 124 | let innerHtml = this.tokens 125 | .map((token) => { 126 | if (token.tokenType === "marker") 127 | return `<${token.closing ? "/" : ""}${token.markerType}>`; 128 | return `${token.innerHtml}`; 129 | }) 130 | .join(""); 131 | 132 | // Browsers don't seem render the background color of completely empty lines, 133 | // so if the rendered inner HTML code is empty and we want to mark the line, 134 | // we need to add some content to make the background color visible. 135 | // To keep the copy & paste result unchanged at the same time, we add an empty span 136 | // and attach a CSS class that displays a space inside a ::before pseudo-element. 137 | if (!innerHtml && this.getLineMarkerType() !== undefined) 138 | innerHtml = ''; 139 | 140 | return `${this.beforeClassValue}${classValue}${this.afterClassValue}${innerHtml}${this.afterTokens}`; 141 | } 142 | 143 | getLineMarkerType(): MarkerType { 144 | return MarkerTypeOrder.find( 145 | (markerType) => markerType && this.classes.has(markerType.toString()) 146 | ); 147 | } 148 | 149 | setLineMarkerType(newType: MarkerType) { 150 | // Remove all existing marker type classes (if any) 151 | MarkerTypeOrder.forEach( 152 | (markerType) => markerType && this.classes.delete(markerType.toString()) 153 | ); 154 | 155 | if (newType === undefined) return; 156 | this.classes.add(newType.toString()); 157 | } 158 | 159 | private getInlineMarkingDefinitionMatches( 160 | inlineMarking: InlineMarkingDefinition 161 | ) { 162 | const markedRanges: MarkedRange[] = []; 163 | 164 | if (inlineMarking.text) { 165 | let idx = this.textLine.indexOf(inlineMarking.text, 0); 166 | while (idx > -1) { 167 | markedRanges.push({ 168 | markerType: inlineMarking.markerType, 169 | start: idx, 170 | end: idx + inlineMarking.text.length, 171 | }); 172 | idx = this.textLine.indexOf( 173 | inlineMarking.text, 174 | idx + inlineMarking.text.length 175 | ); 176 | } 177 | return markedRanges; 178 | } 179 | 180 | if (inlineMarking.regExp) { 181 | const matches = this.textLine.matchAll(inlineMarking.regExp); 182 | for (const match of matches) { 183 | const fullMatchIndex = match.index as number; 184 | // Read the start and end ranges from the `indices` property, 185 | // which is made available through the RegExp flag `d` 186 | // (and unfortunately not recognized by TypeScript) 187 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 188 | let groupIndices = (match as any).indices as ( 189 | | [start: number, end: number] 190 | | null 191 | )[]; 192 | // If accessing the group indices is unsupported, use fallback logic 193 | if (!groupIndices || !groupIndices.length) { 194 | // Try to find the position of each capture group match inside the full match 195 | groupIndices = match.map((groupValue) => { 196 | const groupIndex = groupValue ? match[0].indexOf(groupValue) : -1; 197 | if (groupIndex === -1) return null; 198 | const groupStart = fullMatchIndex + groupIndex; 199 | const groupEnd = groupStart + groupValue.length; 200 | return [groupStart, groupEnd]; 201 | }); 202 | } 203 | // Remove null group indices 204 | groupIndices = groupIndices.filter((range) => range); 205 | // If there are no non-null indices, use the full match instead 206 | if (!groupIndices.length) { 207 | groupIndices = [[fullMatchIndex, fullMatchIndex + match[0].length]]; 208 | } 209 | // If there are multiple non-null indices, remove the first one 210 | // as it is the full match and we only want to mark capture groups 211 | if (groupIndices.length > 1) { 212 | groupIndices.shift(); 213 | } 214 | // Create marked ranges from all remaining group indices 215 | groupIndices.forEach((range) => { 216 | if (!range) return; 217 | markedRanges.push({ 218 | markerType: inlineMarking.markerType, 219 | start: range[0], 220 | end: range[1], 221 | }); 222 | }); 223 | } 224 | return markedRanges; 225 | } 226 | 227 | throw new Error( 228 | `Missing matching logic for inlineMarking=${JSON.stringify( 229 | inlineMarking 230 | )}` 231 | ); 232 | } 233 | 234 | private textPositionToTokenPosition(textPosition: number): InsertionPoint { 235 | for (const [tokenIndex, token] of this.tokens.entries()) { 236 | if (token.tokenType !== "syntax") continue; 237 | 238 | if (textPosition === token.textStart) { 239 | return { 240 | tokenIndex, 241 | innerHtmlOffset: 0, 242 | }; 243 | } 244 | 245 | // The text position is inside the current token 246 | if (textPosition > token.textStart && textPosition < token.textEnd) { 247 | // NOTE: We used to escape the string before `indexOf` as rehype would escape HTML entities 248 | // at render-time, causing the text position to shift. However, with rehype-optimize-static, 249 | // the HTML is preserved as is, so we don't have to anticipate for the shift anymore. 250 | const innerHtmlOffset = ( 251 | token.text.slice(0, textPosition - token.textStart) + 252 | // Insert our special character at textPosition 253 | "\n" + 254 | token.text.slice(textPosition - token.textStart) 255 | ).indexOf("\n"); 256 | 257 | return { 258 | tokenIndex, 259 | innerHtmlOffset, 260 | }; 261 | } 262 | } 263 | 264 | // If we arrive here, the position is after the last token 265 | return { 266 | tokenIndex: this.tokens.length, 267 | innerHtmlOffset: 0, 268 | }; 269 | } 270 | 271 | private insertMarkerTokenAtPosition( 272 | position: InsertionPoint, 273 | markerToken: MarkerToken 274 | ) { 275 | // Insert the new token inside the given token by splitting it 276 | if (position.innerHtmlOffset > 0) { 277 | const insideToken = this.tokens[position.tokenIndex]; 278 | if (insideToken.tokenType !== "syntax") 279 | throw new Error( 280 | `Cannot insert a marker token inside a token of type "${insideToken.tokenType}"!` 281 | ); 282 | 283 | const newInnerHtmlBeforeMarker = insideToken.innerHtml.slice( 284 | 0, 285 | position.innerHtmlOffset 286 | ); 287 | const tokenAfterMarker = { 288 | ...insideToken, 289 | innerHtml: insideToken.innerHtml.slice(position.innerHtmlOffset), 290 | }; 291 | insideToken.innerHtml = newInnerHtmlBeforeMarker; 292 | const newTokens: InlineToken[] = [markerToken]; 293 | // Only add the inside token if it still has contents after splitting 294 | if (tokenAfterMarker.innerHtml.length) newTokens.push(tokenAfterMarker); 295 | this.tokens.splice(position.tokenIndex + 1, 0, ...newTokens); 296 | return; 297 | } 298 | 299 | // Insert the new token before the given token 300 | this.tokens.splice(position.tokenIndex, 0, markerToken); 301 | } 302 | 303 | private flattenMarkedRanges(markedRanges: MarkedRange[]): MarkedRange[] { 304 | const flattenedRanges: MarkedRange[] = []; 305 | const sortedRanges = [...markedRanges].sort((a, b) => a.start - b.start); 306 | const posInRange = (pos: number): { idx: number; range?: MarkedRange } => { 307 | for (let idx = 0; idx < flattenedRanges.length; idx++) { 308 | const range = flattenedRanges[idx]; 309 | if (pos < range.end) 310 | return { 311 | idx, 312 | range: pos >= range.start ? range : undefined, 313 | }; 314 | } 315 | // After the last element 316 | return { 317 | idx: flattenedRanges.length, 318 | }; 319 | }; 320 | 321 | MarkerTypeOrder.forEach((markerType) => { 322 | sortedRanges 323 | .filter((range) => range.markerType === markerType) 324 | .forEach((rangeToAdd) => { 325 | // Clone range to avoid overriding values of the original object 326 | rangeToAdd = { ...rangeToAdd }; 327 | 328 | // Get insertion position for the start and end of rangeToAdd 329 | const posStart = posInRange(rangeToAdd.start); 330 | const posEnd = posInRange(rangeToAdd.end); 331 | 332 | const newElements: MarkedRange[] = [rangeToAdd]; 333 | 334 | // rangeToAdd starts inside an existing range and their start points differ 335 | if (posStart.range && rangeToAdd.start !== posStart.range.start) { 336 | if (posStart.range.markerType === rangeToAdd.markerType) { 337 | rangeToAdd.start = posStart.range.start; 338 | } else { 339 | newElements.unshift({ 340 | ...posStart.range, 341 | end: rangeToAdd.start, 342 | }); 343 | } 344 | } 345 | 346 | // rangeToAdd ends inside an existing range and their end points differ 347 | if (posEnd.range && rangeToAdd.end !== posEnd.range.end) { 348 | if (posEnd.range.markerType === rangeToAdd.markerType) { 349 | rangeToAdd.end = posEnd.range.end; 350 | } else { 351 | newElements.push({ 352 | ...posEnd.range, 353 | start: rangeToAdd.end, 354 | }); 355 | } 356 | } 357 | 358 | flattenedRanges.splice( 359 | posStart.idx, 360 | posEnd.idx - posStart.idx + 1, 361 | ...newElements 362 | ); 363 | }); 364 | }); 365 | 366 | return flattenedRanges; 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/utils/syntax-highlighting-theme.ts: -------------------------------------------------------------------------------- 1 | import type { ShikiConfig } from "astro"; 2 | 3 | const red = { 0: "#ff657c" }; 4 | const yellow = { 0: "#EBCB8B", 1: "#ffbd2e" }; 5 | const blue = { 0: "#66adff", 1: "#5E81AC" }; 6 | const green = { 0: "#16c082" }; 7 | const cyan = { 0: "#23b1af" }; 8 | const grey = { 0: "#d8dee9", 1: "#c7c5d3", 2: "#aba8bd", 9: "#312749" }; 9 | 10 | const foregroundPrimary = grey[0]; 11 | const backgroundPrimary = grey[9]; 12 | 13 | export const theme: ShikiConfig["theme"] = { 14 | name: "Star Gazer", 15 | type: "dark", 16 | fg: foregroundPrimary, 17 | bg: backgroundPrimary, 18 | settings: [ 19 | { 20 | settings: { 21 | foreground: foregroundPrimary, 22 | background: backgroundPrimary, 23 | }, 24 | }, 25 | { 26 | scope: "emphasis", 27 | settings: { 28 | fontStyle: "italic", 29 | }, 30 | }, 31 | { 32 | scope: "strong", 33 | settings: { 34 | fontStyle: "bold", 35 | }, 36 | }, 37 | { 38 | name: "Comment", 39 | scope: "comment", 40 | settings: { 41 | foreground: grey[2], 42 | }, 43 | }, 44 | { 45 | name: "Constant Character", 46 | scope: "constant.character", 47 | settings: { 48 | foreground: yellow[0], 49 | }, 50 | }, 51 | { 52 | name: "Constant Character Escape", 53 | scope: "constant.character.escape", 54 | settings: { 55 | foreground: yellow[0], 56 | }, 57 | }, 58 | { 59 | name: "Constant Language", 60 | scope: "constant.language", 61 | settings: { 62 | foreground: red[0], 63 | }, 64 | }, 65 | { 66 | name: "Constant Numeric", 67 | scope: "constant.numeric", 68 | settings: { 69 | foreground: yellow[0], 70 | }, 71 | }, 72 | { 73 | name: "Constant Regexp", 74 | scope: "constant.regexp", 75 | settings: { 76 | foreground: yellow[0], 77 | }, 78 | }, 79 | { 80 | name: "Entity Name Class/Type", 81 | scope: ["entity.name.class", "entity.name.type.class"], 82 | settings: { 83 | foreground: yellow[1], 84 | }, 85 | }, 86 | { 87 | name: "Entity Name Function", 88 | scope: "entity.name.function", 89 | settings: { 90 | foreground: blue[0], 91 | }, 92 | }, 93 | { 94 | name: "Entity Name Tag", 95 | scope: "entity.name.tag", 96 | settings: { 97 | foreground: red[0], 98 | }, 99 | }, 100 | { 101 | name: "Entity Other Attribute Name", 102 | scope: "entity.other.attribute-name", 103 | settings: { 104 | foreground: yellow[1], 105 | }, 106 | }, 107 | { 108 | name: "Entity Other Inherited Class", 109 | scope: "entity.other.inherited-class", 110 | settings: { 111 | fontStyle: "bold", 112 | foreground: yellow[1], 113 | }, 114 | }, 115 | { 116 | name: "Invalid Deprecated", 117 | scope: "invalid.deprecated", 118 | settings: { 119 | foreground: foregroundPrimary, 120 | background: yellow[0], 121 | }, 122 | }, 123 | { 124 | name: "Invalid Illegal", 125 | scope: "invalid.illegal", 126 | settings: { 127 | foreground: foregroundPrimary, 128 | background: red[0], 129 | }, 130 | }, 131 | { 132 | name: "Keyword", 133 | scope: "keyword", 134 | settings: { 135 | foreground: red[0], 136 | }, 137 | }, 138 | { 139 | name: "Keyword Operator", 140 | scope: "keyword.operator", 141 | settings: { 142 | foreground: red[0], 143 | }, 144 | }, 145 | { 146 | name: "Keyword Other New", 147 | scope: "keyword.other.new", 148 | settings: { 149 | foreground: red[0], 150 | }, 151 | }, 152 | { 153 | name: "Markup Bold", 154 | scope: "markup.bold", 155 | settings: { 156 | fontStyle: "bold", 157 | }, 158 | }, 159 | { 160 | name: "Markup Changed", 161 | scope: "markup.changed", 162 | settings: { 163 | foreground: yellow[0], 164 | }, 165 | }, 166 | { 167 | name: "Markup Deleted", 168 | scope: "markup.deleted", 169 | settings: { 170 | foreground: red[0], 171 | }, 172 | }, 173 | { 174 | name: "Markup Inserted", 175 | scope: "markup.inserted", 176 | settings: { 177 | foreground: green[0], 178 | }, 179 | }, 180 | { 181 | name: "Meta Preprocessor", 182 | scope: "meta.preprocessor", 183 | settings: { 184 | foreground: blue[1], 185 | }, 186 | }, 187 | { 188 | name: "Punctuation", 189 | scope: "punctuation", 190 | settings: { 191 | foreground: grey[1], 192 | }, 193 | }, 194 | { 195 | name: "Punctuation Definition Parameters", 196 | scope: [ 197 | "punctuation.definition.method-parameters", 198 | "punctuation.definition.function-parameters", 199 | "punctuation.definition.parameters", 200 | ], 201 | settings: { 202 | foreground: foregroundPrimary, 203 | }, 204 | }, 205 | { 206 | name: "Punctuation Definition Comment", 207 | scope: [ 208 | "punctuation.definition.comment", 209 | "punctuation.end.definition.comment", 210 | "punctuation.start.definition.comment", 211 | ], 212 | settings: { 213 | foreground: grey[2], 214 | }, 215 | }, 216 | { 217 | name: "Misc blocks", 218 | scope: ["source.astro meta.brace.round"], 219 | settings: { 220 | foreground: grey[2], 221 | }, 222 | }, 223 | { 224 | name: "Punctuation Section", 225 | scope: "punctuation.section", 226 | settings: { 227 | foreground: foregroundPrimary, 228 | }, 229 | }, 230 | { 231 | name: "Punctuation Section Embedded", 232 | scope: [ 233 | "punctuation.section.embedded.begin", 234 | "punctuation.section.embedded.end", 235 | ], 236 | settings: { 237 | foreground: red[0], 238 | }, 239 | }, 240 | { 241 | name: "Punctuation Terminator", 242 | scope: "punctuation.terminator", 243 | settings: { 244 | foreground: red[0], 245 | }, 246 | }, 247 | { 248 | name: "Punctuation Variable", 249 | scope: "punctuation.definition.variable", 250 | settings: { 251 | foreground: red[0], 252 | }, 253 | }, 254 | { 255 | name: "Storage", 256 | scope: "storage", 257 | settings: { 258 | foreground: red[0], 259 | }, 260 | }, 261 | { 262 | name: "String", 263 | scope: "string", 264 | settings: { 265 | foreground: green[0], 266 | }, 267 | }, 268 | { 269 | name: "String Regexp", 270 | scope: "string.regexp", 271 | settings: { 272 | foreground: yellow[0], 273 | }, 274 | }, 275 | { 276 | name: "Support Class", 277 | scope: "support.class", 278 | settings: { 279 | foreground: yellow[1], 280 | }, 281 | }, 282 | { 283 | name: "Support Constant", 284 | scope: "support.constant", 285 | settings: { 286 | foreground: red[0], 287 | }, 288 | }, 289 | { 290 | name: "Support Function", 291 | scope: "support.function", 292 | settings: { 293 | foreground: blue[0], 294 | }, 295 | }, 296 | { 297 | name: "Support Function Construct", 298 | scope: "support.function.construct", 299 | settings: { 300 | foreground: red[0], 301 | }, 302 | }, 303 | { 304 | name: "Support Type", 305 | scope: "support.type", 306 | settings: { 307 | foreground: yellow[1], 308 | }, 309 | }, 310 | { 311 | name: "Support Type Exception", 312 | scope: "support.type.exception", 313 | settings: { 314 | foreground: yellow[1], 315 | }, 316 | }, 317 | { 318 | name: "Token Debug", 319 | scope: "token.debug-token", 320 | settings: { 321 | foreground: yellow[0], 322 | }, 323 | }, 324 | { 325 | name: "Token Error", 326 | scope: "token.error-token", 327 | settings: { 328 | foreground: red[0], 329 | }, 330 | }, 331 | { 332 | name: "Token Info", 333 | scope: "token.info-token", 334 | settings: { 335 | foreground: blue[0], 336 | }, 337 | }, 338 | { 339 | name: "Token Warning", 340 | scope: "token.warn-token", 341 | settings: { 342 | foreground: yellow[0], 343 | }, 344 | }, 345 | { 346 | name: "Variable", 347 | scope: "variable.other", 348 | settings: { 349 | foreground: foregroundPrimary, 350 | }, 351 | }, 352 | { 353 | name: "Variable Language", 354 | scope: "variable.language", 355 | settings: { 356 | foreground: red[0], 357 | }, 358 | }, 359 | { 360 | name: "Variable Parameter", 361 | scope: "variable.parameter", 362 | settings: { 363 | foreground: foregroundPrimary, 364 | }, 365 | }, 366 | { 367 | name: "Quotes", 368 | scope: [ 369 | "punctuation.definition.string.begin", 370 | "punctuation.definition.string.end", 371 | ], 372 | settings: { 373 | foreground: green[0], 374 | }, 375 | }, 376 | { 377 | name: "Punctuation ends (ex. semicolons)", 378 | scope: [ 379 | "punctuation.terminator.statement", 380 | "punctuation.terminator.rule", 381 | ], 382 | settings: { 383 | foreground: grey[1], 384 | }, 385 | }, 386 | { 387 | name: "[Astro] Embedded expressions as HTML props", 388 | scope: ["expression.embbeded.astro"], 389 | settings: { 390 | foreground: red[0], 391 | }, 392 | }, 393 | { 394 | name: "[Astro] Embedded expressions as HTML props", 395 | scope: ["expression.embbeded.astro meta.brace"], 396 | settings: { 397 | foreground: grey[1], 398 | }, 399 | }, 400 | { 401 | name: "[C/CPP] Punctuation Separator Pointer-Access", 402 | scope: "punctuation.separator.pointer-access.c", 403 | settings: { 404 | foreground: red[0], 405 | }, 406 | }, 407 | { 408 | name: "[C/CPP] Meta Preprocessor Include", 409 | scope: [ 410 | "source.c meta.preprocessor.include", 411 | "source.c string.quoted.other.lt-gt.include", 412 | ], 413 | settings: { 414 | foreground: yellow[1], 415 | }, 416 | }, 417 | { 418 | name: "[C/CPP] Conditional Directive", 419 | scope: [ 420 | "source.cpp keyword.control.directive.conditional", 421 | "source.cpp punctuation.definition.directive", 422 | "source.c keyword.control.directive.conditional", 423 | "source.c punctuation.definition.directive", 424 | ], 425 | settings: { 426 | foreground: blue[1], 427 | fontStyle: "bold", 428 | }, 429 | }, 430 | { 431 | name: "[CSS] Constant Other Color RGB Value", 432 | scope: "source.css constant.other.color.rgb-value", 433 | settings: { 434 | foreground: foregroundPrimary, 435 | }, 436 | }, 437 | { 438 | name: "[CSS] Property values", 439 | scope: [ 440 | "meta.property-value.css", 441 | "meta.property-list.css", 442 | "source.css keyword.other.unit", 443 | ], 444 | settings: { 445 | foreground: yellow[0], 446 | }, 447 | }, 448 | { 449 | name: "[CSS] Units", 450 | scope: ["source.css keyword.other.unit"], 451 | settings: { 452 | foreground: yellow[0], 453 | }, 454 | }, 455 | { 456 | name: "[CSS] Function variable arguments", 457 | scope: "meta.function.variable.css", 458 | settings: { 459 | foreground: foregroundPrimary, 460 | }, 461 | }, 462 | { 463 | name: "[CSS] Constant in string (ex. data attribute)", 464 | scope: ["string.quoted.double.css", "string.quoted.single.css"], 465 | settings: { 466 | foreground: green[0], 467 | }, 468 | }, 469 | { 470 | name: "[CSS](Function) Meta Property-Value", 471 | scope: "source.css meta.property-value", 472 | settings: { 473 | foreground: blue[0], 474 | }, 475 | }, 476 | { 477 | name: "[CSS] Media Queries", 478 | scope: [ 479 | "source.css keyword.control.at-rule.media", 480 | "source.css keyword.control.at-rule.media punctuation.definition.keyword", 481 | ], 482 | settings: { 483 | foreground: cyan[0], 484 | }, 485 | }, 486 | { 487 | name: "[CSS] Support Type Property Name", 488 | scope: "source.css support.type.property-name", 489 | settings: { 490 | foreground: cyan[0], 491 | }, 492 | }, 493 | { 494 | name: "[diff] Meta Range Context", 495 | scope: "source.diff meta.diff.range.context", 496 | settings: { 497 | foreground: yellow[1], 498 | }, 499 | }, 500 | { 501 | name: "[diff] Meta Header From-File", 502 | scope: "source.diff meta.diff.header.from-file", 503 | settings: { 504 | foreground: yellow[1], 505 | }, 506 | }, 507 | { 508 | name: "[diff] Punctuation Definition From-File", 509 | scope: "source.diff punctuation.definition.from-file", 510 | settings: { 511 | foreground: yellow[1], 512 | }, 513 | }, 514 | { 515 | name: "[diff] Punctuation Definition Range", 516 | scope: "source.diff punctuation.definition.range", 517 | settings: { 518 | foreground: yellow[1], 519 | }, 520 | }, 521 | { 522 | name: "[diff] Punctuation Definition Separator", 523 | scope: "source.diff punctuation.definition.separator", 524 | settings: { 525 | foreground: red[0], 526 | }, 527 | }, 528 | { 529 | name: "[Elixir](JakeBecker.elixir-ls) module names", 530 | scope: "entity.name.type.module.elixir", 531 | settings: { 532 | foreground: yellow[1], 533 | }, 534 | }, 535 | { 536 | name: "[Elixir](JakeBecker.elixir-ls) module attributes", 537 | scope: "variable.other.readwrite.module.elixir", 538 | settings: { 539 | foreground: foregroundPrimary, 540 | fontStyle: "bold", 541 | }, 542 | }, 543 | { 544 | name: "[Elixir](JakeBecker.elixir-ls) atoms", 545 | scope: "constant.other.symbol.elixir", 546 | settings: { 547 | foreground: foregroundPrimary, 548 | fontStyle: "bold", 549 | }, 550 | }, 551 | { 552 | name: "[Elixir](JakeBecker.elixir-ls) modules", 553 | scope: "variable.other.constant.elixir", 554 | settings: { 555 | foreground: yellow[1], 556 | }, 557 | }, 558 | { 559 | name: "[Go] String Format Placeholder", 560 | scope: "source.go constant.other.placeholder.go", 561 | settings: { 562 | foreground: yellow[0], 563 | }, 564 | }, 565 | { 566 | name: "[JavaScript] Decorator", 567 | scope: [ 568 | "source.js punctuation.decorator", 569 | "source.js meta.decorator variable.other.readwrite", 570 | "source.js meta.decorator entity.name.function", 571 | ], 572 | settings: { 573 | foreground: cyan[0], 574 | }, 575 | }, 576 | { 577 | name: "[JavaScript] Meta Object-Literal Key", 578 | scope: "source.js meta.object-literal.key", 579 | settings: { 580 | foreground: blue[0], 581 | }, 582 | }, 583 | { 584 | name: "[JavaScript](JSDoc) Storage Type Class", 585 | scope: "source.js storage.type.class.jsdoc", 586 | settings: { 587 | foreground: yellow[1], 588 | }, 589 | }, 590 | { 591 | name: "[JavaScript] String Template Literals Punctuation", 592 | scope: [ 593 | "source.js string.quoted.template punctuation.quasi.element.begin", 594 | "source.js string.quoted.template punctuation.quasi.element.end", 595 | "source.js string.template punctuation.definition.template-expression", 596 | ], 597 | settings: { 598 | foreground: red[0], 599 | }, 600 | }, 601 | { 602 | name: "[JavaScript] Interpolated String Template Punctuation Functions", 603 | scope: "source.js string.quoted.template meta.method-call.with-arguments", 604 | settings: { 605 | foreground: foregroundPrimary, 606 | }, 607 | }, 608 | { 609 | name: "[JavaScript] String Template Literal Variable", 610 | scope: [ 611 | "source.js string.template meta.template.expression support.variable.property", 612 | "source.js string.template meta.template.expression variable.other.object", 613 | ], 614 | settings: { 615 | foreground: foregroundPrimary, 616 | }, 617 | }, 618 | { 619 | name: "[JavaScript] Support Type Primitive", 620 | scope: "source.js support.type.primitive", 621 | settings: { 622 | foreground: red[0], 623 | }, 624 | }, 625 | { 626 | name: "[JavaScript] Variable Other Object", 627 | scope: "source.js variable.other.object", 628 | settings: { 629 | foreground: foregroundPrimary, 630 | }, 631 | }, 632 | { 633 | name: "[JavaScript] Variable Other Read-Write Alias", 634 | scope: "source.js variable.other.readwrite.alias", 635 | settings: { 636 | foreground: yellow[1], 637 | }, 638 | }, 639 | { 640 | name: "[JavaScript] Parentheses in Template Strings", 641 | scope: [ 642 | "source.js meta.embedded.line meta.brace.square", 643 | "source.js meta.embedded.line meta.brace.round", 644 | /* Required for extension `mgmcdermott.vscode-language-babel`. */ 645 | "source.js string.quoted.template meta.brace.square", 646 | "source.js string.quoted.template meta.brace.round", 647 | ], 648 | settings: { 649 | foreground: foregroundPrimary, 650 | }, 651 | }, 652 | { 653 | name: "[JavaScript] Braces", 654 | scope: [ 655 | "source.astro meta.brace.square", 656 | "source.astro meta.brace.round", 657 | ], 658 | settings: { 659 | foreground: grey[2], 660 | }, 661 | }, 662 | { 663 | name: "[HTML] Constant Character Entity", 664 | scope: "text.html.basic constant.character.entity.html", 665 | settings: { 666 | foreground: yellow[1], 667 | }, 668 | }, 669 | { 670 | name: "[HTML] Constant Other Inline-Data", 671 | scope: "text.html.basic constant.other.inline-data", 672 | settings: { 673 | foreground: cyan[0], 674 | fontStyle: "italic", 675 | }, 676 | }, 677 | { 678 | name: "[HTML] Meta Tag SGML Doctype", 679 | scope: "text.html.basic meta.tag.sgml.doctype", 680 | settings: { 681 | foreground: blue[1], 682 | }, 683 | }, 684 | { 685 | name: "[HTML] Punctuation Definition Entity", 686 | scope: "text.html.basic punctuation.definition.entity", 687 | settings: { 688 | foreground: red[0], 689 | }, 690 | }, 691 | { 692 | name: "[INI] Entity Name Section Group-Title", 693 | scope: "source.properties entity.name.section.group-title.ini", 694 | settings: { 695 | foreground: blue[0], 696 | }, 697 | }, 698 | { 699 | name: "[INI] Punctuation Separator Key-Value", 700 | scope: "source.properties punctuation.separator.key-value.ini", 701 | settings: { 702 | foreground: red[0], 703 | }, 704 | }, 705 | { 706 | name: "[Markdown] Markup Fenced Code Block", 707 | scope: [ 708 | "text.html.markdown markup.fenced_code.block", 709 | "text.html.markdown markup.fenced_code.block punctuation.definition", 710 | ], 711 | settings: { 712 | foreground: yellow[1], 713 | }, 714 | }, 715 | { 716 | name: "[Markdown] Markup Heading", 717 | scope: "markup.heading", 718 | settings: { 719 | foreground: blue[0], 720 | }, 721 | }, 722 | { 723 | name: "[Markdown] Markup Inline", 724 | scope: [ 725 | "text.html.markdown markup.inline.raw", 726 | "text.html.markdown markup.inline.raw punctuation.definition.raw", 727 | ], 728 | settings: { 729 | foreground: yellow[1], 730 | }, 731 | }, 732 | { 733 | name: "[Markdown] Markup Italic", 734 | scope: "text.html.markdown markup.italic", 735 | settings: { 736 | fontStyle: "italic", 737 | }, 738 | }, 739 | { 740 | name: "[Markdown] Markup Link", 741 | scope: "text.html.markdown markup.underline.link", 742 | settings: { 743 | fontStyle: "underline", 744 | }, 745 | }, 746 | { 747 | name: "[Markdown] Markup List Numbered/Unnumbered", 748 | scope: "text.html.markdown beginning.punctuation.definition.list", 749 | settings: { 750 | foreground: red[0], 751 | }, 752 | }, 753 | { 754 | name: "[Markdown] Markup Quote Punctuation Definition", 755 | scope: "text.html.markdown beginning.punctuation.definition.quote", 756 | settings: { 757 | foreground: yellow[1], 758 | }, 759 | }, 760 | { 761 | name: "[Markdown] Markup Quote Punctuation Definition", 762 | scope: "text.html.markdown markup.quote", 763 | settings: { 764 | foreground: grey[2], 765 | }, 766 | }, 767 | { 768 | name: "[Markdown] Markup Math Constant", 769 | scope: "text.html.markdown constant.character.math.tex", 770 | settings: { 771 | foreground: red[0], 772 | }, 773 | }, 774 | { 775 | name: "[Markdown] Markup Math Definition Marker", 776 | scope: [ 777 | "text.html.markdown punctuation.definition.math.begin", 778 | "text.html.markdown punctuation.definition.math.end", 779 | ], 780 | settings: { 781 | foreground: blue[0], 782 | }, 783 | }, 784 | { 785 | name: "[Markdown] Markup Math Function Definition Marker", 786 | scope: "text.html.markdown punctuation.definition.function.math.tex", 787 | settings: { 788 | foreground: blue[0], 789 | }, 790 | }, 791 | { 792 | name: "[Markdown] Markup Math Operator", 793 | scope: "text.html.markdown punctuation.math.operator.latex", 794 | settings: { 795 | foreground: red[0], 796 | }, 797 | }, 798 | { 799 | name: "[Markdown] Punctuation Definition Heading", 800 | scope: "text.html.markdown punctuation.definition.heading", 801 | settings: { 802 | foreground: red[0], 803 | }, 804 | }, 805 | { 806 | name: "[Markdown] Punctuation Definition Constant/String", 807 | scope: [ 808 | "text.html.markdown punctuation.definition.constant", 809 | "text.html.markdown punctuation.definition.string", 810 | ], 811 | settings: { 812 | foreground: red[0], 813 | }, 814 | }, 815 | { 816 | name: "[Markdown] String Other Link Description/Title", 817 | scope: [ 818 | "text.html.markdown constant.other.reference.link", 819 | "text.html.markdown string.other.link.description", 820 | "text.html.markdown string.other.link.title", 821 | ], 822 | settings: { 823 | foreground: blue[0], 824 | }, 825 | }, 826 | { 827 | name: "[SCSS] Punctuation Definition Interpolation Bracket Curly", 828 | scope: [ 829 | "source.css.scss punctuation.definition.interpolation.begin.bracket.curly", 830 | "source.css.scss punctuation.definition.interpolation.end.bracket.curly", 831 | ], 832 | settings: { 833 | foreground: red[0], 834 | }, 835 | }, 836 | { 837 | name: "[SCSS] Variable Interpolation", 838 | scope: "source.css.scss variable.interpolation", 839 | settings: { 840 | foreground: foregroundPrimary, 841 | fontStyle: "italic", 842 | }, 843 | }, 844 | { 845 | name: "[TypeScript] Decorators", 846 | scope: [ 847 | "source.ts punctuation.decorator", 848 | "source.ts meta.decorator variable.other.readwrite", 849 | "source.ts meta.decorator entity.name.function", 850 | "source.tsx punctuation.decorator", 851 | "source.tsx meta.decorator variable.other.readwrite", 852 | "source.tsx meta.decorator entity.name.function", 853 | ], 854 | settings: { 855 | foreground: cyan[0], 856 | }, 857 | }, 858 | { 859 | name: "[TypeScript] Object-literal keys", 860 | scope: [ 861 | "source.ts meta.object-literal.key", 862 | "source.tsx meta.object-literal.key", 863 | ], 864 | settings: { 865 | foreground: foregroundPrimary, 866 | }, 867 | }, 868 | { 869 | name: "[TypeScript] Object-literal functions", 870 | scope: [ 871 | "source.ts meta.object-literal.key entity.name.function", 872 | "source.tsx meta.object-literal.key entity.name.function", 873 | ], 874 | settings: { 875 | foreground: blue[0], 876 | }, 877 | }, 878 | { 879 | name: "[TypeScript] Type/Class", 880 | scope: [ 881 | "source.ts support.class", 882 | "source.ts support.type", 883 | "source.ts entity.name.type", 884 | "source.ts entity.name.class", 885 | "source.tsx support.class", 886 | "source.tsx support.type", 887 | "source.tsx entity.name.type", 888 | "source.tsx entity.name.class", 889 | ], 890 | settings: { 891 | foreground: yellow[1], 892 | }, 893 | }, 894 | { 895 | name: "[TypeScript] Static Class Support", 896 | scope: [ 897 | "source.ts support.constant.math", 898 | "source.ts support.constant.dom", 899 | "source.ts support.constant.json", 900 | "source.tsx support.constant.math", 901 | "source.tsx support.constant.dom", 902 | "source.tsx support.constant.json", 903 | ], 904 | settings: { 905 | foreground: yellow[1], 906 | }, 907 | }, 908 | { 909 | name: "[TypeScript] Variables", 910 | scope: ["source.ts support.variable", "source.tsx support.variable"], 911 | settings: { 912 | foreground: foregroundPrimary, 913 | }, 914 | }, 915 | { 916 | name: "[TypeScript] Parentheses in Template Strings", 917 | scope: [ 918 | "source.ts meta.embedded.line meta.brace.square", 919 | "source.ts meta.embedded.line meta.brace.round", 920 | "source.tsx meta.embedded.line meta.brace.square", 921 | "source.tsx meta.embedded.line meta.brace.round", 922 | ], 923 | settings: { 924 | foreground: foregroundPrimary, 925 | }, 926 | }, 927 | { 928 | name: "[XML] Entity Name Tag Namespace", 929 | scope: "text.xml entity.name.tag.namespace", 930 | settings: { 931 | foreground: yellow[1], 932 | }, 933 | }, 934 | { 935 | name: "[XML] Keyword Other Doctype", 936 | scope: "text.xml keyword.other.doctype", 937 | settings: { 938 | foreground: blue[1], 939 | }, 940 | }, 941 | { 942 | name: "[XML] Meta Tag Preprocessor", 943 | scope: "text.xml meta.tag.preprocessor entity.name.tag", 944 | settings: { 945 | foreground: blue[1], 946 | }, 947 | }, 948 | { 949 | name: "[XML] Entity Name Tag Namespace", 950 | scope: [ 951 | "text.xml string.unquoted.cdata", 952 | "text.xml string.unquoted.cdata punctuation.definition.string", 953 | ], 954 | settings: { 955 | foreground: cyan[0], 956 | }, 957 | }, 958 | { 959 | name: "[YAML] Entity Name Tag", 960 | scope: "source.yaml entity.name.tag", 961 | settings: { 962 | foreground: yellow[1], 963 | }, 964 | }, 965 | ], 966 | }; 967 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type MarkerType = "mark" | "ins" | "del" | undefined; 2 | 3 | /** When markers overlap, those with higher indices override lower ones. */ 4 | export const MarkerTypeOrder: MarkerType[] = ["mark", "del", "ins"]; 5 | 6 | export type LineMarkingDefinition = { 7 | markerType: MarkerType; 8 | lines: number[]; 9 | }; 10 | 11 | export type InlineMarkingDefinition = { 12 | markerType: MarkerType; 13 | text?: string; 14 | regExp?: RegExp; 15 | }; 16 | 17 | export type MarkedRange = { 18 | markerType: MarkerType; 19 | start: number; 20 | end: number; 21 | }; 22 | 23 | export type SyntaxToken = { 24 | tokenType: "syntax"; 25 | color: string; 26 | otherStyles: string; 27 | innerHtml: string; 28 | text: string; 29 | textStart: number; 30 | textEnd: number; 31 | }; 32 | 33 | export type MarkerToken = { 34 | tokenType: "marker"; 35 | markerType: MarkerType; 36 | closing?: boolean; 37 | }; 38 | 39 | export type InlineToken = SyntaxToken | MarkerToken; 40 | 41 | export type InsertionPoint = { 42 | tokenIndex: number; 43 | innerHtmlOffset: number; 44 | }; 45 | -------------------------------------------------------------------------------- /src/utils/user-config.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const UserConfigSchema = z.object({ 4 | copyButtonTitle: z 5 | .string() 6 | .describe( 7 | "Title of the copy button." 8 | ).optional(), 9 | copyButtonTooltip: z 10 | .string() 11 | .describe( 12 | "Tooltip of the copy button. Will appear once the content is copied to the clipboard." 13 | ).optional(), 14 | }); 15 | 16 | export const CodeBlocksConfigSchema = UserConfigSchema.strict(); 17 | 18 | export type CodeBlocksConfig = z.infer; 19 | export type CodeBlocksUserConfig = z.input; -------------------------------------------------------------------------------- /src/virtual.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:codeblocks/user-config' { 2 | const Config: import('./utils/user-config').CodeBlocksConfig; 3 | export default Config; 4 | } 5 | declare module 'virtual:codeblocks/project-context' { 6 | export default { root: string }; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*", "virtual.d.ts"], 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "module": "ESNext", 6 | "outDir": "dist", 7 | "target": "ESNext", 8 | "declaration": true, 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "verbatimModuleSyntax": true, 14 | "types": ["astro/client"] 15 | }, 16 | "jsx": "react-jsx" 17 | } --------------------------------------------------------------------------------