Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
radicle-explorer src lib syntax.ts
import type { ElementContent, Root } from "hast";

import onigurumaWASMUrl from "vscode-oniguruma/release/onig.wasm?url";
import sourceAsciiDoc from "@wooorm/starry-night/text.html.asciidoc";
import sourceDockerfile from "@wooorm/starry-night/source.dockerfile";
import sourceErlang from "@wooorm/starry-night/source.erlang";
import sourceSolidity from "@wooorm/starry-night/source.solidity";
import sourceSvelte from "@wooorm/starry-night/source.svelte";
import sourceSass from "@wooorm/starry-night/source.sass";
import sourceToml from "@wooorm/starry-night/source.toml";
import sourceTsx from "@wooorm/starry-night/source.tsx";
import sourceNix from "@wooorm/starry-night/source.nix";
import sourceGitconfig from "@wooorm/starry-night/source.gitconfig";
import sourceGitignore from "@wooorm/starry-night/source.gitignore";
import sourceGitrevlist from "@wooorm/starry-night/source.git-revlist";
import sourceGitattributes from "@wooorm/starry-night/source.gitattributes";
import sourceJson from "@wooorm/starry-night/source.json";
import sourceNpmrc from "@wooorm/starry-night/source.ini.npmrc";
import sourceGradle from "@wooorm/starry-night/source.groovy.gradle";
import sourceBatchfile from "@wooorm/starry-night/source.batchfile";
import sourceEditorconfig from "@wooorm/starry-night/source.editorconfig";
import sourceHaproxyConfig from "@wooorm/starry-night/source.haproxy-config";
import sourceDotenv from "@wooorm/starry-night/source.dotenv";
import sourceZig from "@wooorm/starry-night/source.zig";
import textHtmlVue from "@wooorm/starry-night/text.html.vue";
import textHtmlDjango from "@wooorm/starry-night/text.html.django";
import textRobotsTxt from "@wooorm/starry-night/text.robots-txt";
import textZoneFile from "@wooorm/starry-night/text.zone_file";
import etc from "@wooorm/starry-night/etc";
import goMod from "@wooorm/starry-night/go.mod";
import goSum from "@wooorm/starry-night/go.sum";

import { createStarryNight, common, type Grammar } from "@wooorm/starry-night";

export { type Root };

export const grammars = [
  ...common,
  sourceAsciiDoc,
  sourceToml,
  sourceErlang,
  sourceSolidity,
  sourceSvelte,
  sourceSass,
  sourceTsx,
  sourceDockerfile,
  sourceNix,
  sourceGitconfig,
  sourceGitignore,
  sourceGitrevlist,
  sourceGitattributes,
  sourceNpmrc,
  sourceGradle,
  sourceBatchfile,
  sourceEditorconfig,
  sourceHaproxyConfig,
  sourceDotenv,
  sourceZig,
  textHtmlVue,
  textHtmlDjango,
  textRobotsTxt,
  textZoneFile,
  etc,
  goMod,
  goSum,
  {
    extensions: [".hintrc"],
    names: ["json"],
    patterns: [sourceJson],
    scopeName: "source.json",
  },
  {
    extensions: [
      ".npmignore",
      ".eslintignore",
      ".dockerignore",
      ".nuxtignore",
      ".vscodeignore",
    ],
    names: ["ignore"],
    patterns: [sourceGitignore],
    scopeName: "source.gitignore",
  },
  {
    extensions: [".sample", ".example", ".template"],
    names: [".env.sample", ".env.example", ".env.template"],
    patterns: [sourceDotenv],
    scopeName: "source.dotenv",
  },
  {
    extensions: [".mod"],
    names: ["go.mod"],
    patterns: [goMod],
    scopeName: "go.mode",
  },
  {
    extensions: [".sum"],
    names: ["go.sum"],
    patterns: [goSum],
    scopeName: "go.sum",
  },
  // A grammar that doesn't do any parsing, but needed for files without a known filetype.
  {
    extensions: [""],
    names: ["raw-format"],
    patterns: [],
    scopeName: "text.raw",
  },
] satisfies Grammar[];

let starryNight: Awaited<ReturnType<typeof createStarryNight>>;

export async function highlight(
  content: string,
  grammar: string,
): Promise<Root> {
  if (starryNight === undefined) {
    starryNight = await createStarryNight(grammars, {
      getOnigurumaUrlFetch: () => new URL(onigurumaWASMUrl, import.meta.url),
    });
  }
  const scope = starryNight.flagToScope(grammar);
  return starryNight.highlight(content, scope ?? "text.raw");
}

export function lineNumbersGutter(tree: Root) {
  const replacement: ElementContent[] = [];
  const search = /\r?\n|\r/g;
  let index = -1;
  let start = 0;
  let startTextRemainder = "";
  let lineNumber = 0;

  while (++index < tree.children.length) {
    const child = tree.children[index];

    if (child.type === "text") {
      let textStart = 0;
      let match = search.exec(child.value);

      while (match) {
        // Nodes in this line.
        const line = tree.children.slice(start, index) as ElementContent[];

        // Prepend text from a partial matched earlier text.
        if (startTextRemainder) {
          line.unshift({ type: "text", value: startTextRemainder });
          startTextRemainder = "";
        }

        // Append text from this text.
        if (match.index > textStart) {
          line.push({
            type: "text",
            value: child.value.slice(textStart, match.index),
          });
        }

        // Add a line, and the eol.
        lineNumber += 1;
        replacement.push(createLine(line, lineNumber), {
          type: "text",
          value: match[0],
        });

        start = index + 1;
        textStart = match.index + match[0].length;
        match = search.exec(child.value);
      }

      // If we matched, make sure to not drop the text after the last line ending.
      if (start === index + 1) {
        startTextRemainder = child.value.slice(textStart);
      }
    }
  }

  const line = tree.children.slice(start) as ElementContent[];
  // Prepend text from a partial matched earlier text.
  if (startTextRemainder) {
    line.unshift({ type: "text", value: startTextRemainder });
  }

  if (line.length > 0) {
    lineNumber += 1;
    replacement.push(createLine(line, lineNumber));
  }

  // Replace children with new array.
  tree.children = replacement;

  return tree;
}

function createLine(children: ElementContent[], line: number): ElementContent {
  return {
    type: "element",
    tagName: "tr",
    properties: {
      class: "line",
      id: "L" + line,
    },
    children: [
      {
        type: "element",
        tagName: "td",
        properties: {
          className: "line-number",
        },
        children: [
          {
            type: "element",
            tagName: "a",
            properties: { href: "#L" + line },
            children: [{ type: "text", value: line.toString() }],
          },
        ],
      },
      {
        type: "element",
        tagName: "td",
        properties: {
          className: "line-content",
        },
        children: [
          {
            type: "element",
            tagName: "pre",
            properties: {
              className: "content",
            },
            children,
          },
        ],
      },
    ],
  };
}