Repository: devongovett/tree-sitter-highlight Branch: master Commit: bcde34baa17d Files: 13 Total size: 35.7 KB Directory structure: gitextract_mngzxg9r/ ├── .cargo/ │ └── config.toml ├── .github/ │ └── workflows/ │ └── tag-release.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── bench.js ├── build.rs ├── index.d.ts ├── index.js ├── package.json └── src/ ├── highlight_names.rs └── lib.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [target.'cfg(target_env = "gnu")'] rustflags = ["-C", "link-args=-Wl,-z,nodelete"] [target.arm-unknown-linux-gnueabihf] linker = "arm-linux-gnueabihf-gcc" [target.armv7-unknown-linux-gnueabihf] linker = "arm-linux-gnueabihf-gcc" [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" [target.aarch64-unknown-linux-musl] linker = "aarch64-linux-musl-gcc" ================================================ FILE: .github/workflows/tag-release.yml ================================================ name: tag-release on: release: types: [published] workflow_dispatch: jobs: build-windows: name: windows runs-on: windows-latest steps: - uses: actions/checkout@v1 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true - uses: bahmutov/npm-install@v1.1.0 - name: Build native packages run: yarn build-release - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: bindings-windows path: ./*.node - name: Smoke test run: node -e "require('./')" build-linux-gnu-x64: name: linux-gnu-x64 runs-on: ubuntu-latest container: image: node:20 steps: - uses: actions/checkout@v1 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true - uses: bahmutov/npm-install@v1.1.0 - name: Build native packages run: yarn build-release env: CFLAGS: -std=c99 - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 run: strip ./*.node - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: bindings-linux-gnu-x64 path: ./*.node - name: debug run: ls -l ./*.node - name: Smoke test run: node -e 'require("./")' build-linux-gnu-arm: strategy: fail-fast: false matrix: include: - target: arm-unknown-linux-gnueabihf arch: armhf strip: arm-linux-gnueabihf-strip - target: aarch64-unknown-linux-gnu arch: arm64 strip: aarch64-linux-gnu-strip name: ${{ matrix.target }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true target: ${{ matrix.target }} - name: Install cross compile toolchains run: | sudo apt-get update sudo apt-get install gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu g++-aarch64-linux-gnu -y - uses: bahmutov/npm-install@v1.1.0 - name: Build native packages run: yarn build-release --target ${{ matrix.target }} - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 run: ${{ matrix.strip }} ./*.node - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: ./*.node - name: debug run: ls -l ./*.node - name: Configure binfmt-support run: docker run --rm --privileged multiarch/qemu-user-static:register --reset build-linux-musl: strategy: fail-fast: false matrix: include: - target: x86_64-unknown-linux-musl strip: strip - target: aarch64-unknown-linux-musl strip: aarch64-linux-musl-strip name: ${{ matrix.target }} runs-on: ubuntu-latest container: image: node:20-alpine steps: - uses: actions/checkout@v1 - name: Install build tools run: apk add --no-cache python3 make gcc g++ musl-dev curl tar - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true target: ${{ matrix.target }} - name: Install cross compile toolchains if: ${{ matrix.target == 'aarch64-unknown-linux-musl' }} run: | curl -L -O https://github.com/devongovett/linux-musl-cross/releases/download/v1/aarch64-linux-musl-cross.tgz tar -xzf aarch64-linux-musl-cross.tgz cp -R aarch64-linux-musl-cross/* /usr - uses: bahmutov/npm-install@v1.1.0 - name: Build native packages run: yarn build-release --target ${{ matrix.target }} - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 run: ${{ matrix.strip }} ./*.node - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: ./*.node - name: debug run: ls -l ./*.node - name: Smoke test if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }} run: node -e 'require("./")' build-macos: strategy: fail-fast: false matrix: include: - name: aarch64-apple-darwin target: aarch64-apple-darwin - name: x86_64-apple-darwin target: x86_64-apple-darwin name: ${{ matrix.name }} runs-on: macos-latest steps: - uses: actions/checkout@v1 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true target: ${{ matrix.target }} - uses: bahmutov/npm-install@v1.1.0 - name: Build native packages run: | sudo rm -Rf /Library/Developer/CommandLineTools/SDKs/*; export CC=$(xcrun -f clang); export CXX=$(xcrun -f clang++); SYSROOT=$(xcrun --sdk macosx --show-sdk-path); export CFLAGS="-isysroot $SYSROOT -isystem $SYSROOT"; export MACOSX_DEPLOYMENT_TARGET="10.9"; yarn build-release --target ${{ matrix.target }} - name: Strip debug symbols run: strip -x ./*.node - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: ./*.node - name: debug run: ls -l ./*.node build-and-release: runs-on: ubuntu-latest name: Build and release the tagged version needs: - build-windows - build-linux-musl - build-linux-gnu-arm - build-macos steps: - uses: actions/checkout@v1 - uses: bahmutov/npm-install@v1.1.0 - name: Download artifacts uses: actions/download-artifact@v4 with: path: artifacts - name: Move artifacts run: mv artifacts/*/*.node . - name: Debug run: ls -l ./*.node - run: echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} > .npmrc env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npm publish ================================================ FILE: .gitignore ================================================ /target node_modules Cargo.lock *.node ================================================ FILE: Cargo.toml ================================================ [package] name = "node-tree-sitter-highlight" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] napi = "2" napi-derive = "2" lazy_static = "1.4" tree-sitter-highlight = "0.26.3" tree-sitter-javascript = "0.25.0" tree-sitter-typescript = "0.23.2" tree-sitter-jsdoc = "0.25.0" tree-sitter-json = "0.24.8" tree-sitter-css = "0.25.0" tree-sitter-regex = "0.25.0" tree-sitter-yaml = "0.7.2" tree-sitter-html = "0.23.2" tree-sitter-c = "0.24.1" tree-sitter-bash = "0.25.1" tree-sitter-rust = "0.24.0" [build-dependencies] napi-build = { version = "1" } tree-sitter = "0.26.3" tree-sitter-highlight = "0.26.3" tree-sitter-javascript = "0.25.0" tree-sitter-typescript = "0.23.2" tree-sitter-jsdoc = "0.25.0" tree-sitter-json = "0.24.8" tree-sitter-css = "0.25.0" tree-sitter-regex = "0.25.0" tree-sitter-yaml = "0.7.2" tree-sitter-html = "0.23.2" tree-sitter-c = "0.24.1" tree-sitter-bash = "0.25.1" tree-sitter-rust = "0.24.0" [profile.release] opt-level = 3 lto = true ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Devon Govett Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # tree-sitter-highlight A syntax highlighter for Node.js powered by [Tree Sitter](https://github.com/tree-sitter/tree-sitter). Written in Rust. ## Usage The following will output HTML: ```js const treeSitter = require('tree-sitter-highlight'); treeSitter.highlight('const foo = "hi";', treeSitter.Language.JS); // => '...' ``` You can also output a [HAST](https://github.com/syntax-tree/hast) AST, which is useful for integrating with Markdown or MDX processors (e.g. Remark). ```js treeSitter.highlightHast('const foo = "hi";', treeSitter.Language.JS); // => {type: 'element', children: [...]} ``` ## Themes The output HTML will contain CSS class names for various tokens. These will depend on the language, but there are several common names used across languages. Here is a basic example theme: ```css .keyword { color: purple; } .function { color: blue; } .type { color: pink; } .string { color: green; } .number { color: brown; } .operator { color: gray; } .comment { color: lightgray; } ``` Inspect the generated output HTML and design your CSS accordingly. ## License MIT ================================================ FILE: bench.js ================================================ const {Benchmark} = require("tiny-benchy"); const ts = require('./'); let input = ` function Example() { let alertDismiss = (close) => { close(); alert('Dialog dismissed.'); }; return ( Info {(close) => ( alertDismiss(close)}> Version Info Version 1.0.0, Copyright 2020 )} ); } `; let suite = new Benchmark({iterations: 50}); suite.add('html', () => { ts.highlight(input, ts.Language.JSX); }); suite.add('hast', () => { ts.highlightHast(input, ts.Language.JSX); }); suite.run(); ================================================ FILE: build.rs ================================================ use tree_sitter::Language; extern crate napi_build; fn main() { let mut queries = String::new(); queries.push_str(tree_sitter_javascript::HIGHLIGHT_QUERY); queries.push_str(tree_sitter_javascript::JSX_HIGHLIGHT_QUERY); let mut highlight_names = Vec::new(); add_highlight_names( tree_sitter_javascript::LANGUAGE.into(), &queries, &mut highlight_names, ); add_highlight_names( tree_sitter_typescript::LANGUAGE_TSX.into(), tree_sitter_typescript::HIGHLIGHTS_QUERY, &mut highlight_names, ); add_highlight_names( tree_sitter_css::LANGUAGE.into(), tree_sitter_css::HIGHLIGHTS_QUERY, &mut highlight_names, ); add_highlight_names( tree_sitter_regex::LANGUAGE.into(), tree_sitter_regex::HIGHLIGHTS_QUERY, &mut highlight_names, ); add_highlight_names( tree_sitter_jsdoc::LANGUAGE.into(), tree_sitter_jsdoc::HIGHLIGHTS_QUERY, &mut highlight_names, ); add_highlight_names( tree_sitter_json::LANGUAGE.into(), tree_sitter_json::HIGHLIGHTS_QUERY, &mut highlight_names, ); add_highlight_names( tree_sitter_yaml::LANGUAGE.into(), tree_sitter_yaml::HIGHLIGHTS_QUERY, &mut highlight_names, ); add_highlight_names( tree_sitter_html::LANGUAGE.into(), tree_sitter_html::HIGHLIGHTS_QUERY, &mut highlight_names, ); add_highlight_names( tree_sitter_c::LANGUAGE.into(), tree_sitter_c::HIGHLIGHT_QUERY, &mut highlight_names, ); add_highlight_names( tree_sitter_bash::LANGUAGE.into(), tree_sitter_bash::HIGHLIGHT_QUERY, &mut highlight_names, ); add_highlight_names( tree_sitter_rust::LANGUAGE.into(), tree_sitter_rust::HIGHLIGHTS_QUERY, &mut highlight_names, ); highlight_names.sort(); let html_attrs: Vec = highlight_names .iter() .map(|s| format!("class=\"{}\"", s.replace('.', " "))) .collect(); let class_names: Vec = highlight_names .iter() .map(|s| s.replace('.', " ")) .collect(); std::fs::write( "src/highlight_names.rs", format!( "pub const HIGHLIGHT_NAMES: &[&str] = &{:#?};\n\npub const HTML_ATTRS: &[&str] = &{:#?};\n\npub const CLASS_NAMES: &[&str] = &{:#?};\n", highlight_names, html_attrs, class_names ), ) .expect("write error"); napi_build::setup(); } fn add_highlight_names(lang: Language, source: &str, highlights: &mut Vec) { let query = tree_sitter::Query::new(&lang, source).unwrap(); for capture in query.capture_names() { if !highlights.iter().any(|h| h == capture) { highlights.push(capture.to_string()); } } } ================================================ FILE: index.d.ts ================================================ /* tslint:disable */ /* eslint-disable */ /* auto-generated by NAPI-RS */ export const enum Language { JS = 0, JSX = 1, TS = 2, TSX = 3, JSON = 4, YAML = 5, CSS = 6, HTML = 7, Regex = 8, JsDoc = 9, C = 10, Bash = 11, Rust = 12 } export declare function highlight(code: string, language: Language): string export interface HastProperties { className: string } export interface HastNode { type: string tagName: string properties: HastProperties children: Array } export interface HastTextNode { type: string value: string } export declare function highlightHast(code: string, language: Language): HastNode ================================================ FILE: index.js ================================================ /* tslint:disable */ /* eslint-disable */ /* prettier-ignore */ /* auto-generated by NAPI-RS */ const { existsSync, readFileSync } = require('fs') const { join } = require('path') const { platform, arch } = process let nativeBinding = null let localFileExisted = false let loadError = null function isMusl() { // For Node 10 if (!process.report || typeof process.report.getReport !== 'function') { try { const lddPath = require('child_process').execSync('which ldd').toString().trim() return readFileSync(lddPath, 'utf8').includes('musl') } catch (e) { return true } } else { const { glibcVersionRuntime } = process.report.getReport().header return !glibcVersionRuntime } } switch (platform) { case 'android': switch (arch) { case 'arm64': localFileExisted = existsSync(join(__dirname, 'tree-sitter-highlight.android-arm64.node')) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.android-arm64.node') } else { nativeBinding = require('tree-sitter-highlight-android-arm64') } } catch (e) { loadError = e } break case 'arm': localFileExisted = existsSync(join(__dirname, 'tree-sitter-highlight.android-arm-eabi.node')) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.android-arm-eabi.node') } else { nativeBinding = require('tree-sitter-highlight-android-arm-eabi') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on Android ${arch}`) } break case 'win32': switch (arch) { case 'x64': localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.win32-x64-msvc.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.win32-x64-msvc.node') } else { nativeBinding = require('tree-sitter-highlight-win32-x64-msvc') } } catch (e) { loadError = e } break case 'ia32': localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.win32-ia32-msvc.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.win32-ia32-msvc.node') } else { nativeBinding = require('tree-sitter-highlight-win32-ia32-msvc') } } catch (e) { loadError = e } break case 'arm64': localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.win32-arm64-msvc.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.win32-arm64-msvc.node') } else { nativeBinding = require('tree-sitter-highlight-win32-arm64-msvc') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on Windows: ${arch}`) } break case 'darwin': localFileExisted = existsSync(join(__dirname, 'tree-sitter-highlight.darwin-universal.node')) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.darwin-universal.node') } else { nativeBinding = require('tree-sitter-highlight-darwin-universal') } break } catch {} switch (arch) { case 'x64': localFileExisted = existsSync(join(__dirname, 'tree-sitter-highlight.darwin-x64.node')) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.darwin-x64.node') } else { nativeBinding = require('tree-sitter-highlight-darwin-x64') } } catch (e) { loadError = e } break case 'arm64': localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.darwin-arm64.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.darwin-arm64.node') } else { nativeBinding = require('tree-sitter-highlight-darwin-arm64') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on macOS: ${arch}`) } break case 'freebsd': if (arch !== 'x64') { throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) } localFileExisted = existsSync(join(__dirname, 'tree-sitter-highlight.freebsd-x64.node')) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.freebsd-x64.node') } else { nativeBinding = require('tree-sitter-highlight-freebsd-x64') } } catch (e) { loadError = e } break case 'linux': switch (arch) { case 'x64': if (isMusl()) { localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.linux-x64-musl.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.linux-x64-musl.node') } else { nativeBinding = require('tree-sitter-highlight-linux-x64-musl') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.linux-x64-gnu.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.linux-x64-gnu.node') } else { nativeBinding = require('tree-sitter-highlight-linux-x64-gnu') } } catch (e) { loadError = e } } break case 'arm64': if (isMusl()) { localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.linux-arm64-musl.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.linux-arm64-musl.node') } else { nativeBinding = require('tree-sitter-highlight-linux-arm64-musl') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.linux-arm64-gnu.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.linux-arm64-gnu.node') } else { nativeBinding = require('tree-sitter-highlight-linux-arm64-gnu') } } catch (e) { loadError = e } } break case 'arm': if (isMusl()) { localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.linux-arm-musleabihf.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.linux-arm-musleabihf.node') } else { nativeBinding = require('tree-sitter-highlight-linux-arm-musleabihf') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.linux-arm-gnueabihf.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.linux-arm-gnueabihf.node') } else { nativeBinding = require('tree-sitter-highlight-linux-arm-gnueabihf') } } catch (e) { loadError = e } } break case 'riscv64': if (isMusl()) { localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.linux-riscv64-musl.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.linux-riscv64-musl.node') } else { nativeBinding = require('tree-sitter-highlight-linux-riscv64-musl') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.linux-riscv64-gnu.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.linux-riscv64-gnu.node') } else { nativeBinding = require('tree-sitter-highlight-linux-riscv64-gnu') } } catch (e) { loadError = e } } break case 's390x': localFileExisted = existsSync( join(__dirname, 'tree-sitter-highlight.linux-s390x-gnu.node') ) try { if (localFileExisted) { nativeBinding = require('./tree-sitter-highlight.linux-s390x-gnu.node') } else { nativeBinding = require('tree-sitter-highlight-linux-s390x-gnu') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on Linux: ${arch}`) } break default: throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) } if (!nativeBinding) { if (loadError) { throw loadError } throw new Error(`Failed to load native binding`) } const { Language, highlight, highlightHast } = nativeBinding module.exports.Language = Language module.exports.highlight = highlight module.exports.highlightHast = highlightHast ================================================ FILE: package.json ================================================ { "name": "tree-sitter-highlight", "version": "1.1.2", "description": "A syntax highlighter for Node powered by Tree Sitter. Written in Rust.", "repository": "https://github.com/devongovett/tree-sitter-highlight", "author": "Devon Govett ", "license": "MIT", "napi": { "name": "tree-sitter-highlight" }, "files": [ "*.node", "index.js", "index.d.ts" ], "scripts": { "build": "napi build --platform", "build-release": "napi build --platform --release" }, "devDependencies": { "@napi-rs/cli": "^2.0.0", "tiny-benchy": "^2.1.0" } } ================================================ FILE: src/highlight_names.rs ================================================ pub const HIGHLIGHT_NAMES: &[&str] = &[ "attribute", "boolean", "character.special", "comment", "comment.documentation", "constant", "constant.builtin", "constant.character", "constructor", "delimiter", "embedded", "escape", "function", "function.builtin", "function.macro", "function.method", "function.special", "keyword", "label", "number", "operator", "property", "punctuation.bracket", "punctuation.delimiter", "punctuation.special", "string", "string.special", "string.special.key", "tag", "tag.error", "type", "type.builtin", "variable", "variable.builtin", "variable.parameter", ]; pub const HTML_ATTRS: &[&str] = &[ "class=\"attribute\"", "class=\"boolean\"", "class=\"character special\"", "class=\"comment\"", "class=\"comment documentation\"", "class=\"constant\"", "class=\"constant builtin\"", "class=\"constant character\"", "class=\"constructor\"", "class=\"delimiter\"", "class=\"embedded\"", "class=\"escape\"", "class=\"function\"", "class=\"function builtin\"", "class=\"function macro\"", "class=\"function method\"", "class=\"function special\"", "class=\"keyword\"", "class=\"label\"", "class=\"number\"", "class=\"operator\"", "class=\"property\"", "class=\"punctuation bracket\"", "class=\"punctuation delimiter\"", "class=\"punctuation special\"", "class=\"string\"", "class=\"string special\"", "class=\"string special key\"", "class=\"tag\"", "class=\"tag error\"", "class=\"type\"", "class=\"type builtin\"", "class=\"variable\"", "class=\"variable builtin\"", "class=\"variable parameter\"", ]; pub const CLASS_NAMES: &[&str] = &[ "attribute", "boolean", "character special", "comment", "comment documentation", "constant", "constant builtin", "constant character", "constructor", "delimiter", "embedded", "escape", "function", "function builtin", "function macro", "function method", "function special", "keyword", "label", "number", "operator", "property", "punctuation bracket", "punctuation delimiter", "punctuation special", "string", "string special", "string special key", "tag", "tag error", "type", "type builtin", "variable", "variable builtin", "variable parameter", ]; ================================================ FILE: src/lib.rs ================================================ mod highlight_names; use highlight_names::{CLASS_NAMES, HIGHLIGHT_NAMES, HTML_ATTRS}; use lazy_static::lazy_static; use napi::bindgen_prelude::*; use napi_derive::napi; use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer}; #[napi] pub enum Language { JS, JSX, TS, TSX, JSON, YAML, CSS, HTML, Regex, JsDoc, C, Bash, Rust, } macro_rules! language { ($mod: ident, $name: literal, $highlights: ident) => {{ let mut config = HighlightConfiguration::new($mod::LANGUAGE.into(), $name, $mod::$highlights, "", "") .unwrap(); config.configure(HIGHLIGHT_NAMES); config }}; ($mod: ident, $name: literal, $highlights: ident, $injections: ident) => {{ let mut config = HighlightConfiguration::new( $mod::LANGUAGE.into(), $name, $mod::$highlights, $mod::$injections, "", ) .unwrap(); config.configure(HIGHLIGHT_NAMES); config }}; } lazy_static! { static ref JS_CONFIG: HighlightConfiguration = { let mut config = HighlightConfiguration::new( tree_sitter_javascript::LANGUAGE.into(), "javascript", tree_sitter_javascript::HIGHLIGHT_QUERY, tree_sitter_javascript::INJECTIONS_QUERY, tree_sitter_javascript::LOCALS_QUERY, ) .unwrap(); config.configure(HIGHLIGHT_NAMES); config }; static ref JSX_CONFIG: HighlightConfiguration = { let mut highlights = tree_sitter_javascript::JSX_HIGHLIGHT_QUERY.to_owned(); highlights.push_str(tree_sitter_javascript::HIGHLIGHT_QUERY); let mut config = HighlightConfiguration::new( tree_sitter_javascript::LANGUAGE.into(), "jsx", &highlights, tree_sitter_javascript::INJECTIONS_QUERY, tree_sitter_javascript::LOCALS_QUERY, ) .unwrap(); config.configure(HIGHLIGHT_NAMES); config }; static ref TS_CONFIG: HighlightConfiguration = { let mut highlights = tree_sitter_typescript::HIGHLIGHTS_QUERY.to_owned(); highlights.push_str(tree_sitter_javascript::HIGHLIGHT_QUERY); let mut locals = tree_sitter_typescript::LOCALS_QUERY.to_owned(); locals.push_str(tree_sitter_javascript::LOCALS_QUERY); let mut config = HighlightConfiguration::new( tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), "typescript", &highlights, tree_sitter_javascript::INJECTIONS_QUERY, &locals, ) .unwrap(); config.configure(HIGHLIGHT_NAMES); config }; static ref TSX_CONFIG: HighlightConfiguration = { let mut highlights = tree_sitter_javascript::JSX_HIGHLIGHT_QUERY.to_owned(); highlights.push_str(tree_sitter_typescript::HIGHLIGHTS_QUERY); highlights.push_str(tree_sitter_javascript::HIGHLIGHT_QUERY); let mut locals = tree_sitter_typescript::LOCALS_QUERY.to_owned(); locals.push_str(tree_sitter_javascript::LOCALS_QUERY); let mut config = HighlightConfiguration::new( tree_sitter_typescript::LANGUAGE_TSX.into(), "tsx", &highlights, tree_sitter_javascript::INJECTIONS_QUERY, &locals, ) .unwrap(); config.configure(HIGHLIGHT_NAMES); config }; static ref JSDOC_CONFIG: HighlightConfiguration = language!(tree_sitter_jsdoc, "jsdoc", HIGHLIGHTS_QUERY); static ref JSON_CONFIG: HighlightConfiguration = language!(tree_sitter_json, "json", HIGHLIGHTS_QUERY); static ref YAML_CONFIG: HighlightConfiguration = language!(tree_sitter_yaml, "yaml", HIGHLIGHTS_QUERY); static ref CSS_CONFIG: HighlightConfiguration = language!(tree_sitter_css, "css", HIGHLIGHTS_QUERY); static ref HTML_CONFIG: HighlightConfiguration = language!(tree_sitter_html, "html", INJECTIONS_QUERY); static ref REGEX_CONFIG: HighlightConfiguration = language!(tree_sitter_regex, "regex", HIGHLIGHTS_QUERY); static ref C_CONFIG: HighlightConfiguration = language!(tree_sitter_c, "c", HIGHLIGHT_QUERY); static ref BASH_CONFIG: HighlightConfiguration = language!(tree_sitter_bash, "bash", HIGHLIGHT_QUERY); static ref RUST_CONFIG: HighlightConfiguration = language!(tree_sitter_rust, "rust", HIGHLIGHTS_QUERY); } impl Language { fn highlight_config(&self) -> &'static HighlightConfiguration { match self { Language::JS => &*JS_CONFIG, Language::JSX => &*JSX_CONFIG, Language::TS => &*TS_CONFIG, Language::TSX => &*TSX_CONFIG, Language::JSON => &*JSON_CONFIG, Language::YAML => &*YAML_CONFIG, Language::CSS => &*CSS_CONFIG, Language::HTML => &*HTML_CONFIG, Language::Regex => &*REGEX_CONFIG, Language::JsDoc => &*JSDOC_CONFIG, Language::C => &*C_CONFIG, Language::Bash => &*BASH_CONFIG, Language::Rust => &*RUST_CONFIG, } } fn from_name(name: &str) -> Option { Some(match name { "js" | "javascript" => Language::JS, "jsx" => Language::JSX, "ts" | "typescript" => Language::TS, "tsx" => Language::TSX, "json" => Language::JSON, "yaml" => Language::YAML, "css" => Language::CSS, "html" => Language::HTML, "regex" => Language::Regex, "jsdoc" => Language::JsDoc, "c" => Language::C, "bash" => Language::Bash, "sh" => Language::Bash, "rust" => Language::Rust, "rs" => Language::Rust, _ => return None, }) } } #[napi] pub fn highlight(code: String, language: Language) -> String { let config = language.highlight_config(); let mut highlighter = Highlighter::new(); let highlights = highlighter .highlight(&config, code.as_bytes(), None, |lang| { Language::from_name(lang).map(|l| l.highlight_config()) }) .unwrap(); let mut renderer = HtmlRenderer::new(); renderer .render(highlights, code.as_bytes(), &|highlight, res| { res.extend_from_slice(HTML_ATTRS[highlight.0].as_bytes()) }) .unwrap(); unsafe { String::from_utf8_unchecked(renderer.html) } } #[derive(Debug)] #[napi(object)] pub struct HastProperties { pub class_name: String, } #[derive(Debug)] #[napi(object)] pub struct HastNode { #[napi(js_name = "type")] pub kind: String, pub tag_name: String, pub properties: HastProperties, pub children: Vec>, } #[derive(Debug)] #[napi(object)] pub struct HastTextNode { #[napi(js_name = "type")] pub kind: String, pub value: String, } #[napi] pub fn highlight_hast(code: String, language: Language) -> HastNode { let config = language.highlight_config(); let mut highlighter = Highlighter::new(); let highlights = highlighter .highlight(&config, code.as_bytes(), None, |lang| { Language::from_name(lang).map(|l| l.highlight_config()) }) .unwrap(); let mut stack = Vec::new(); stack.push(HastNode { kind: "element".into(), tag_name: "span".into(), properties: HastProperties { class_name: "source".into(), }, children: Vec::new(), }); for event in highlights { match event.unwrap() { HighlightEvent::HighlightStart(highlight) => { let node = HastNode { kind: "element".into(), tag_name: "span".into(), properties: HastProperties { class_name: CLASS_NAMES[highlight.0].to_owned(), }, children: Vec::new(), }; stack.push(node); } HighlightEvent::Source { start, end } => { let slice = &code[start..end]; let parent = stack.last_mut().unwrap(); if let Some(Either::B(text_node)) = parent.children.last_mut() { text_node.value.push_str(slice); } else { let text_node = HastTextNode { kind: "text".into(), value: slice.into(), }; parent.children.push(Either::B(text_node)); } } HighlightEvent::HighlightEnd => { let node = stack.pop().unwrap(); let parent = stack.last_mut().unwrap(); parent.children.push(Either::A(node)); } } } stack.pop().unwrap() }