Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Move syntax highlighting to web client
Sebastian Martinez committed 3 years ago
commit 569b6601341af8cd9fb31232bf6bde4ecd1c8a25
parent 795ab5253d6429ee330ef390b99b533e847be4de
17 files changed +846 -88
modified index.html
@@ -20,6 +20,7 @@
    <link rel="stylesheet" type="text/css" href="/colors.css" />
    <link rel="stylesheet" type="text/css" href="/elevations.css" />
    <link rel="stylesheet" type="text/css" href="/layout.css" />
+
    <link rel="stylesheet" type="text/css" href="/prettylights.css" />
    <link rel="stylesheet" type="text/css" href="/index.css" />
    <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
    <link rel="icon" href="/favicon.ico" />
modified package-lock.json
@@ -12,10 +12,13 @@
        "@radicle/gray-matter": "4.1.0",
        "@stardazed/streams": "^3.1.0",
        "@walletconnect/client": "^1.8.0",
+
        "@wooorm/starry-night": "^1.4.2",
        "buffer": "^6.0.3",
        "dompurify": "^2.4.1",
        "ethers": "^5.7.2",
        "events": "^3.3.0",
+
        "hast-util-to-dom": "^3.1.0",
+
        "hast-util-to-html": "^8.0.3",
        "katex": "^0.16.4",
        "lodash": "^4.17.21",
        "lru-cache": "^7.14.1",
@@ -1343,6 +1346,14 @@
        "@types/trusted-types": "*"
      }
    },
+
    "node_modules/@types/hast": {
+
      "version": "2.3.4",
+
      "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
+
      "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==",
+
      "dependencies": {
+
        "@types/unist": "*"
+
      }
+
    },
    "node_modules/@types/json-schema": {
      "version": "7.0.11",
      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@@ -1430,6 +1441,11 @@
      "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
      "dev": true
    },
+
    "node_modules/@types/unist": {
+
      "version": "2.0.6",
+
      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
+
      "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
+
    },
    "node_modules/@typescript-eslint/eslint-plugin": {
      "version": "5.46.0",
      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.0.tgz",
@@ -1804,6 +1820,21 @@
        "@walletconnect/window-getters": "^1.0.0"
      }
    },
+
    "node_modules/@wooorm/starry-night": {
+
      "version": "1.4.2",
+
      "resolved": "https://registry.npmjs.org/@wooorm/starry-night/-/starry-night-1.4.2.tgz",
+
      "integrity": "sha512-SkhIweiThgUK+KmvZPR2VCSnA1m0awTi/jPHey71MtR+jZG6phmcc9F9b0xpvxmHFFiKqxkgD/UKSVWVua+xrw==",
+
      "dependencies": {
+
        "@types/hast": "^2.0.0",
+
        "import-meta-resolve": "^2.0.0",
+
        "vscode-oniguruma": "^1.0.0",
+
        "vscode-textmate": "^7.0.0"
+
      },
+
      "funding": {
+
        "type": "github",
+
        "url": "https://github.com/sponsors/wooorm"
+
      }
+
    },
    "node_modules/acorn": {
      "version": "8.8.1",
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
@@ -2105,6 +2136,15 @@
        "node": ">=6"
      }
    },
+
    "node_modules/ccount": {
+
      "version": "2.0.1",
+
      "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+
      "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+
      "funding": {
+
        "type": "github",
+
        "url": "https://github.com/sponsors/wooorm"
+
      }
+
    },
    "node_modules/chai": {
      "version": "4.3.7",
      "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz",
@@ -2135,6 +2175,24 @@
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
+
    "node_modules/character-entities-html4": {
+
      "version": "2.1.0",
+
      "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+
      "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+
      "funding": {
+
        "type": "github",
+
        "url": "https://github.com/sponsors/wooorm"
+
      }
+
    },
+
    "node_modules/character-entities-legacy": {
+
      "version": "3.0.0",
+
      "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+
      "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+
      "funding": {
+
        "type": "github",
+
        "url": "https://github.com/sponsors/wooorm"
+
      }
+
    },
    "node_modules/charenc": {
      "version": "0.0.2",
      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
@@ -2219,6 +2277,15 @@
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true
    },
+
    "node_modules/comma-separated-tokens": {
+
      "version": "2.0.3",
+
      "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+
      "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+
      "funding": {
+
        "type": "github",
+
        "url": "https://github.com/sponsors/wooorm"
+
      }
+
    },
    "node_modules/commander": {
      "version": "8.3.0",
      "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
@@ -3226,6 +3293,62 @@
        "minimalistic-assert": "^1.0.1"
      }
    },
+
    "node_modules/hast-util-is-element": {
+
      "version": "2.1.2",
+
      "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz",
+
      "integrity": "sha512-thjnlGAnwP8ef/GSO1Q8BfVk2gundnc2peGQqEg2kUt/IqesiGg/5mSwN2fE7nLzy61pg88NG6xV+UrGOrx9EA==",
+
      "dependencies": {
+
        "@types/hast": "^2.0.0",
+
        "@types/unist": "^2.0.0"
+
      },
+
      "funding": {
+
        "type": "opencollective",
+
        "url": "https://opencollective.com/unified"
+
      }
+
    },
+
    "node_modules/hast-util-to-dom": {
+
      "version": "3.1.0",
+
      "resolved": "https://registry.npmjs.org/hast-util-to-dom/-/hast-util-to-dom-3.1.0.tgz",
+
      "integrity": "sha512-ZGrDF2qJDSWIHqos/YWbzeehVI4AYPz1RGWklNrKf2mJPCVe+cLnnsSzheSpHeIrJ/KFfz9tOzMwyphy+EIUUg==",
+
      "dependencies": {
+
        "property-information": "^6.0.0",
+
        "web-namespaces": "^2.0.0"
+
      },
+
      "funding": {
+
        "type": "opencollective",
+
        "url": "https://opencollective.com/unified"
+
      }
+
    },
+
    "node_modules/hast-util-to-html": {
+
      "version": "8.0.3",
+
      "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.3.tgz",
+
      "integrity": "sha512-/D/E5ymdPYhHpPkuTHOUkSatxr4w1ZKrZsG0Zv/3C2SRVT0JFJG53VS45AMrBtYk0wp5A7ksEhiC8QaOZM95+A==",
+
      "dependencies": {
+
        "@types/hast": "^2.0.0",
+
        "ccount": "^2.0.0",
+
        "comma-separated-tokens": "^2.0.0",
+
        "hast-util-is-element": "^2.0.0",
+
        "hast-util-whitespace": "^2.0.0",
+
        "html-void-elements": "^2.0.0",
+
        "property-information": "^6.0.0",
+
        "space-separated-tokens": "^2.0.0",
+
        "stringify-entities": "^4.0.2",
+
        "unist-util-is": "^5.0.0"
+
      },
+
      "funding": {
+
        "type": "opencollective",
+
        "url": "https://opencollective.com/unified"
+
      }
+
    },
+
    "node_modules/hast-util-whitespace": {
+
      "version": "2.0.0",
+
      "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz",
+
      "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==",
+
      "funding": {
+
        "type": "opencollective",
+
        "url": "https://opencollective.com/unified"
+
      }
+
    },
    "node_modules/he": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -3245,6 +3368,15 @@
        "minimalistic-crypto-utils": "^1.0.1"
      }
    },
+
    "node_modules/html-void-elements": {
+
      "version": "2.0.1",
+
      "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
+
      "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
+
      "funding": {
+
        "type": "github",
+
        "url": "https://github.com/sponsors/wooorm"
+
      }
+
    },
    "node_modules/iconv-lite": {
      "version": "0.6.3",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -3301,6 +3433,15 @@
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
+
    "node_modules/import-meta-resolve": {
+
      "version": "2.2.0",
+
      "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-2.2.0.tgz",
+
      "integrity": "sha512-CpPOtiCHxP9HdtDM5F45tNiAe66Cqlv3f5uHoJjt+KlaLrUh9/Wz9vepADZ78SlqEo62aDWZtj9ydMGXV+CPnw==",
+
      "funding": {
+
        "type": "github",
+
        "url": "https://github.com/sponsors/wooorm"
+
      }
+
    },
    "node_modules/imurmurhash": {
      "version": "0.1.4",
      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -4083,6 +4224,15 @@
        "svelte": "^3.2.0"
      }
    },
+
    "node_modules/property-information": {
+
      "version": "6.2.0",
+
      "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz",
+
      "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==",
+
      "funding": {
+
        "type": "github",
+
        "url": "https://github.com/sponsors/wooorm"
+
      }
+
    },
    "node_modules/punycode": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -4500,6 +4650,15 @@
      "deprecated": "Please use @jridgewell/sourcemap-codec instead",
      "dev": true
    },
+
    "node_modules/space-separated-tokens": {
+
      "version": "2.0.2",
+
      "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+
      "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+
      "funding": {
+
        "type": "github",
+
        "url": "https://github.com/sponsors/wooorm"
+
      }
+
    },
    "node_modules/split-on-first": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
@@ -4525,6 +4684,19 @@
        "safe-buffer": "~5.2.0"
      }
    },
+
    "node_modules/stringify-entities": {
+
      "version": "4.0.3",
+
      "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz",
+
      "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==",
+
      "dependencies": {
+
        "character-entities-html4": "^2.0.0",
+
        "character-entities-legacy": "^3.0.0"
+
      },
+
      "funding": {
+
        "type": "github",
+
        "url": "https://github.com/sponsors/wooorm"
+
      }
+
    },
    "node_modules/strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -4892,6 +5064,15 @@
        "node": ">=4.2.0"
      }
    },
+
    "node_modules/unist-util-is": {
+
      "version": "5.1.1",
+
      "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz",
+
      "integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==",
+
      "funding": {
+
        "type": "opencollective",
+
        "url": "https://opencollective.com/unified"
+
      }
+
    },
    "node_modules/universalify": {
      "version": "0.1.2",
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -5045,6 +5226,25 @@
        }
      }
    },
+
    "node_modules/vscode-oniguruma": {
+
      "version": "1.7.0",
+
      "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
+
      "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA=="
+
    },
+
    "node_modules/vscode-textmate": {
+
      "version": "7.0.4",
+
      "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-7.0.4.tgz",
+
      "integrity": "sha512-9hJp0xL7HW1Q5OgGe03NACo7yiCTMEk3WU/rtKXUbncLtdg6rVVNJnHwD88UhbIYU2KoxY0Dih0x+kIsmUKn2A=="
+
    },
+
    "node_modules/web-namespaces": {
+
      "version": "2.0.1",
+
      "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+
      "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
+
      "funding": {
+
        "type": "github",
+
        "url": "https://github.com/sponsors/wooorm"
+
      }
+
    },
    "node_modules/webidl-conversions": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -5914,6 +6114,14 @@
        "@types/trusted-types": "*"
      }
    },
+
    "@types/hast": {
+
      "version": "2.3.4",
+
      "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
+
      "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==",
+
      "requires": {
+
        "@types/unist": "*"
+
      }
+
    },
    "@types/json-schema": {
      "version": "7.0.11",
      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@@ -6001,6 +6209,11 @@
      "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
      "dev": true
    },
+
    "@types/unist": {
+
      "version": "2.0.6",
+
      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
+
      "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
+
    },
    "@typescript-eslint/eslint-plugin": {
      "version": "5.46.0",
      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.0.tgz",
@@ -6300,6 +6513,17 @@
        "@walletconnect/window-getters": "^1.0.0"
      }
    },
+
    "@wooorm/starry-night": {
+
      "version": "1.4.2",
+
      "resolved": "https://registry.npmjs.org/@wooorm/starry-night/-/starry-night-1.4.2.tgz",
+
      "integrity": "sha512-SkhIweiThgUK+KmvZPR2VCSnA1m0awTi/jPHey71MtR+jZG6phmcc9F9b0xpvxmHFFiKqxkgD/UKSVWVua+xrw==",
+
      "requires": {
+
        "@types/hast": "^2.0.0",
+
        "import-meta-resolve": "^2.0.0",
+
        "vscode-oniguruma": "^1.0.0",
+
        "vscode-textmate": "^7.0.0"
+
      }
+
    },
    "acorn": {
      "version": "8.8.1",
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
@@ -6519,6 +6743,11 @@
      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
      "dev": true
    },
+
    "ccount": {
+
      "version": "2.0.1",
+
      "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+
      "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="
+
    },
    "chai": {
      "version": "4.3.7",
      "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz",
@@ -6540,6 +6769,16 @@
      "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==",
      "dev": true
    },
+
    "character-entities-html4": {
+
      "version": "2.1.0",
+
      "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+
      "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="
+
    },
+
    "character-entities-legacy": {
+
      "version": "3.0.0",
+
      "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+
      "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="
+
    },
    "charenc": {
      "version": "0.0.2",
      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
@@ -6603,6 +6842,11 @@
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true
    },
+
    "comma-separated-tokens": {
+
      "version": "2.0.3",
+
      "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+
      "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="
+
    },
    "commander": {
      "version": "8.3.0",
      "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
@@ -7411,6 +7655,46 @@
        "minimalistic-assert": "^1.0.1"
      }
    },
+
    "hast-util-is-element": {
+
      "version": "2.1.2",
+
      "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz",
+
      "integrity": "sha512-thjnlGAnwP8ef/GSO1Q8BfVk2gundnc2peGQqEg2kUt/IqesiGg/5mSwN2fE7nLzy61pg88NG6xV+UrGOrx9EA==",
+
      "requires": {
+
        "@types/hast": "^2.0.0",
+
        "@types/unist": "^2.0.0"
+
      }
+
    },
+
    "hast-util-to-dom": {
+
      "version": "3.1.0",
+
      "resolved": "https://registry.npmjs.org/hast-util-to-dom/-/hast-util-to-dom-3.1.0.tgz",
+
      "integrity": "sha512-ZGrDF2qJDSWIHqos/YWbzeehVI4AYPz1RGWklNrKf2mJPCVe+cLnnsSzheSpHeIrJ/KFfz9tOzMwyphy+EIUUg==",
+
      "requires": {
+
        "property-information": "^6.0.0",
+
        "web-namespaces": "^2.0.0"
+
      }
+
    },
+
    "hast-util-to-html": {
+
      "version": "8.0.3",
+
      "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.3.tgz",
+
      "integrity": "sha512-/D/E5ymdPYhHpPkuTHOUkSatxr4w1ZKrZsG0Zv/3C2SRVT0JFJG53VS45AMrBtYk0wp5A7ksEhiC8QaOZM95+A==",
+
      "requires": {
+
        "@types/hast": "^2.0.0",
+
        "ccount": "^2.0.0",
+
        "comma-separated-tokens": "^2.0.0",
+
        "hast-util-is-element": "^2.0.0",
+
        "hast-util-whitespace": "^2.0.0",
+
        "html-void-elements": "^2.0.0",
+
        "property-information": "^6.0.0",
+
        "space-separated-tokens": "^2.0.0",
+
        "stringify-entities": "^4.0.2",
+
        "unist-util-is": "^5.0.0"
+
      }
+
    },
+
    "hast-util-whitespace": {
+
      "version": "2.0.0",
+
      "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz",
+
      "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg=="
+
    },
    "he": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -7427,6 +7711,11 @@
        "minimalistic-crypto-utils": "^1.0.1"
      }
    },
+
    "html-void-elements": {
+
      "version": "2.0.1",
+
      "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
+
      "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A=="
+
    },
    "iconv-lite": {
      "version": "0.6.3",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -7457,6 +7746,11 @@
        "resolve-from": "^4.0.0"
      }
    },
+
    "import-meta-resolve": {
+
      "version": "2.2.0",
+
      "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-2.2.0.tgz",
+
      "integrity": "sha512-CpPOtiCHxP9HdtDM5F45tNiAe66Cqlv3f5uHoJjt+KlaLrUh9/Wz9vepADZ78SlqEo62aDWZtj9ydMGXV+CPnw=="
+
    },
    "imurmurhash": {
      "version": "0.1.4",
      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -8009,6 +8303,11 @@
      "dev": true,
      "requires": {}
    },
+
    "property-information": {
+
      "version": "6.2.0",
+
      "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz",
+
      "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg=="
+
    },
    "punycode": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -8294,6 +8593,11 @@
      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
      "dev": true
    },
+
    "space-separated-tokens": {
+
      "version": "2.0.2",
+
      "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+
      "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="
+
    },
    "split-on-first": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
@@ -8313,6 +8617,15 @@
        "safe-buffer": "~5.2.0"
      }
    },
+
    "stringify-entities": {
+
      "version": "4.0.3",
+
      "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz",
+
      "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==",
+
      "requires": {
+
        "character-entities-html4": "^2.0.0",
+
        "character-entities-legacy": "^3.0.0"
+
      }
+
    },
    "strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -8553,6 +8866,11 @@
      "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
      "dev": true
    },
+
    "unist-util-is": {
+
      "version": "5.1.1",
+
      "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz",
+
      "integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ=="
+
    },
    "universalify": {
      "version": "0.1.2",
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -8627,6 +8945,21 @@
        "vite": "^3.0.0 || ^4.0.0"
      }
    },
+
    "vscode-oniguruma": {
+
      "version": "1.7.0",
+
      "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
+
      "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA=="
+
    },
+
    "vscode-textmate": {
+
      "version": "7.0.4",
+
      "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-7.0.4.tgz",
+
      "integrity": "sha512-9hJp0xL7HW1Q5OgGe03NACo7yiCTMEk3WU/rtKXUbncLtdg6rVVNJnHwD88UhbIYU2KoxY0Dih0x+kIsmUKn2A=="
+
    },
+
    "web-namespaces": {
+
      "version": "2.0.1",
+
      "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+
      "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="
+
    },
    "webidl-conversions": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
modified package.json
@@ -47,10 +47,13 @@
    "@radicle/gray-matter": "4.1.0",
    "@stardazed/streams": "^3.1.0",
    "@walletconnect/client": "^1.8.0",
+
    "@wooorm/starry-night": "^1.4.2",
    "buffer": "^6.0.3",
    "dompurify": "^2.4.1",
    "ethers": "^5.7.2",
    "events": "^3.3.0",
+
    "hast-util-to-dom": "^3.1.0",
+
    "hast-util-to-html": "^8.0.3",
    "katex": "^0.16.4",
    "lodash": "^4.17.21",
    "lru-cache": "^7.14.1",
added public/prettylights.css
@@ -0,0 +1,190 @@
+
/* This is a theme distributed by `starry-night`.
+
 * It’s based on what GitHub uses on their site.
+
 * See <https://github.com/wooorm/starry-night> for more info. */
+
:root {
+
  --color-prettylights-syntax-comment: #6e7781;
+
  --color-prettylights-syntax-constant: #0550ae;
+
  --color-prettylights-syntax-entity: #8250df;
+
  --color-prettylights-syntax-storage-modifier-import: #24292f;
+
  --color-prettylights-syntax-entity-tag: #116329;
+
  --color-prettylights-syntax-keyword: #cf222e;
+
  --color-prettylights-syntax-string: #0a3069;
+
  --color-prettylights-syntax-variable: #953800;
+
  --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
+
  --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
+
  --color-prettylights-syntax-invalid-illegal-bg: #82071e;
+
  --color-prettylights-syntax-carriage-return-text: #f6f8fa;
+
  --color-prettylights-syntax-carriage-return-bg: #cf222e;
+
  --color-prettylights-syntax-string-regexp: #116329;
+
  --color-prettylights-syntax-markup-list: #3b2300;
+
  --color-prettylights-syntax-markup-heading: #0550ae;
+
  --color-prettylights-syntax-markup-italic: #24292f;
+
  --color-prettylights-syntax-markup-bold: #24292f;
+
  --color-prettylights-syntax-markup-deleted-text: #82071e;
+
  --color-prettylights-syntax-markup-deleted-bg: #ffebe9;
+
  --color-prettylights-syntax-markup-inserted-text: #116329;
+
  --color-prettylights-syntax-markup-inserted-bg: #dafbe1;
+
  --color-prettylights-syntax-markup-changed-text: #953800;
+
  --color-prettylights-syntax-markup-changed-bg: #ffd8b5;
+
  --color-prettylights-syntax-markup-ignored-text: #eaeef2;
+
  --color-prettylights-syntax-markup-ignored-bg: #0550ae;
+
  --color-prettylights-syntax-meta-diff-range: #8250df;
+
  --color-prettylights-syntax-brackethighlighter-angle: #57606a;
+
  --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
+
  --color-prettylights-syntax-constant-other-reference-link: #0a3069;
+
}
+

+
@media (prefers-color-scheme: dark) {
+
  :root {
+
    --color-prettylights-syntax-comment: #8b949e;
+
    --color-prettylights-syntax-constant: #79c0ff;
+
    --color-prettylights-syntax-entity: #d2a8ff;
+
    --color-prettylights-syntax-storage-modifier-import: #c9d1d9;
+
    --color-prettylights-syntax-entity-tag: #7ee787;
+
    --color-prettylights-syntax-keyword: #ff7b72;
+
    --color-prettylights-syntax-string: #a5d6ff;
+
    --color-prettylights-syntax-variable: #ffa657;
+
    --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
+
    --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
+
    --color-prettylights-syntax-invalid-illegal-bg: #8e1519;
+
    --color-prettylights-syntax-carriage-return-text: #f0f6fc;
+
    --color-prettylights-syntax-carriage-return-bg: #b62324;
+
    --color-prettylights-syntax-string-regexp: #7ee787;
+
    --color-prettylights-syntax-markup-list: #f2cc60;
+
    --color-prettylights-syntax-markup-heading: #1f6feb;
+
    --color-prettylights-syntax-markup-italic: #c9d1d9;
+
    --color-prettylights-syntax-markup-bold: #c9d1d9;
+
    --color-prettylights-syntax-markup-deleted-text: #ffdcd7;
+
    --color-prettylights-syntax-markup-deleted-bg: #67060c;
+
    --color-prettylights-syntax-markup-inserted-text: #aff5b4;
+
    --color-prettylights-syntax-markup-inserted-bg: #033a16;
+
    --color-prettylights-syntax-markup-changed-text: #ffdfb6;
+
    --color-prettylights-syntax-markup-changed-bg: #5a1e02;
+
    --color-prettylights-syntax-markup-ignored-text: #c9d1d9;
+
    --color-prettylights-syntax-markup-ignored-bg: #1158c7;
+
    --color-prettylights-syntax-meta-diff-range: #d2a8ff;
+
    --color-prettylights-syntax-brackethighlighter-angle: #8b949e;
+
    --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
+
    --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
+
  }
+
}
+

+
.pl-c {
+
  color: var(--color-prettylights-syntax-comment);
+
}
+

+
.pl-c1,
+
.pl-s .pl-v {
+
  color: var(--color-prettylights-syntax-constant);
+
}
+

+
.pl-e,
+
.pl-en {
+
  color: var(--color-prettylights-syntax-entity);
+
}
+

+
.pl-smi,
+
.pl-s .pl-s1 {
+
  color: var(--color-prettylights-syntax-storage-modifier-import);
+
}
+

+
.pl-ent {
+
  color: var(--color-prettylights-syntax-entity-tag);
+
}
+

+
.pl-k {
+
  color: var(--color-prettylights-syntax-keyword);
+
}
+

+
.pl-s,
+
.pl-pds,
+
.pl-s .pl-pse .pl-s1,
+
.pl-sr,
+
.pl-sr .pl-cce,
+
.pl-sr .pl-sre,
+
.pl-sr .pl-sra {
+
  color: var(--color-prettylights-syntax-string);
+
}
+

+
.pl-v,
+
.pl-smw {
+
  color: var(--color-prettylights-syntax-variable);
+
}
+

+
.pl-bu {
+
  color: var(--color-prettylights-syntax-brackethighlighter-unmatched);
+
}
+

+
.pl-ii {
+
  color: var(--color-prettylights-syntax-invalid-illegal-text);
+
  background-color: var(--color-prettylights-syntax-invalid-illegal-bg);
+
}
+

+
.pl-c2 {
+
  color: var(--color-prettylights-syntax-carriage-return-text);
+
  background-color: var(--color-prettylights-syntax-carriage-return-bg);
+
}
+

+
.pl-sr .pl-cce {
+
  font-weight: bold;
+
  color: var(--color-prettylights-syntax-string-regexp);
+
}
+

+
.pl-ml {
+
  color: var(--color-prettylights-syntax-markup-list);
+
}
+

+
.pl-mh,
+
.pl-mh .pl-en,
+
.pl-ms {
+
  font-weight: bold;
+
  color: var(--color-prettylights-syntax-markup-heading);
+
}
+

+
.pl-mi {
+
  font-style: italic;
+
  color: var(--color-prettylights-syntax-markup-italic);
+
}
+

+
.pl-mb {
+
  font-weight: bold;
+
  color: var(--color-prettylights-syntax-markup-bold);
+
}
+

+
.pl-md {
+
  color: var(--color-prettylights-syntax-markup-deleted-text);
+
  background-color: var(--color-prettylights-syntax-markup-deleted-bg);
+
}
+

+
.pl-mi1 {
+
  color: var(--color-prettylights-syntax-markup-inserted-text);
+
  background-color: var(--color-prettylights-syntax-markup-inserted-bg);
+
}
+

+
.pl-mc {
+
  color: var(--color-prettylights-syntax-markup-changed-text);
+
  background-color: var(--color-prettylights-syntax-markup-changed-bg);
+
}
+

+
.pl-mi2 {
+
  color: var(--color-prettylights-syntax-markup-ignored-text);
+
  background-color: var(--color-prettylights-syntax-markup-ignored-bg);
+
}
+

+
.pl-mdr {
+
  font-weight: bold;
+
  color: var(--color-prettylights-syntax-meta-diff-range);
+
}
+

+
.pl-ba {
+
  color: var(--color-prettylights-syntax-brackethighlighter-angle);
+
}
+

+
.pl-sg {
+
  color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);
+
}
+

+
.pl-corl {
+
  text-decoration: underline;
+
  color: var(--color-prettylights-syntax-constant-other-reference-link);
+
}
modified src/Markdown.svelte
@@ -4,6 +4,7 @@
  import dompurify from "dompurify";
  import matter from "@radicle/gray-matter";
  import { base } from "@app/router";
+
  import { highlight } from "@app/syntax";
  import {
    markdownExtensions as extensions,
    renderer,
@@ -14,6 +15,7 @@
  } from "@app/utils";
  import { marked } from "marked";
  import { onMount } from "svelte";
+
  import { toDom } from "hast-util-to-dom";

  export let content: string;
  export let doc = matter(content);
@@ -29,7 +31,7 @@
    // eslint-disable-next-line @typescript-eslint/naming-convention
    dompurify.sanitize(marked.parse(content), { SANITIZE_DOM: false });

-
  onMount(() => {
+
  onMount(async () => {
    // Don't underline <a> tags that contain images.
    const elems = container.querySelectorAll("a");

@@ -59,6 +61,31 @@
        });
      }
    }
+

+
    // Replaces code blocks in the background with highlighted code.
+
    const prefix = "language-";
+
    const nodes = Array.from(document.body.querySelectorAll("pre code"));
+

+
    const treeChanges: Promise<void>[] = [];
+

+
    for (const node of nodes) {
+
      const className = Array.from(node.classList).find(name =>
+
        name.startsWith(prefix),
+
      );
+
      if (!className) continue;
+

+
      treeChanges.push(
+
        highlight(node.textContent ?? "", className.slice(prefix.length))
+
          .then(tree => {
+
            if (tree) {
+
              node.replaceChildren(toDom(tree, { fragment: true }));
+
            }
+
          })
+
          .catch(e => console.warn("Not able to highlight code block", e)),
+
      );
+
    }
+

+
    await Promise.allSettled(treeChanges);
  });
</script>

@@ -160,7 +187,7 @@
  .markdown :global(code) {
    font-family: var(--font-family-monospace);
    font-size: var(--font-size-regular);
-
    color: var(--color-secondary-6);
+
    color: var(--color-foreground);
    background-color: var(--color-foreground-2);
    border-radius: 0.5rem;
    padding: 0.125rem 0.25rem;
modified src/base/projects/Blob.svelte
@@ -1,12 +1,15 @@
<script lang="ts">
  import type { Blob } from "@app/project";
+
  import type { MaybeHighlighted } from "@app/syntax";
  import type { ProjectRoute } from "@app/router/definitions";

  import HeaderToggleLabel from "@app/base/projects/HeaderToggleLabel.svelte";
-
  import ProjectLink from "@app/router/ProjectLink.svelte";
  import Readme from "@app/base/projects/Readme.svelte";
+
  import { afterUpdate, beforeUpdate, onMount } from "svelte";
+
  import { highlight } from "@app/syntax";
  import { isMarkdownPath, scrollIntoView, twemoji } from "@app/utils";
-
  import { onMount } from "svelte";
+
  import { lineNumbersGutter } from "@app/syntax";
+
  import { toHtml } from "hast-util-to-html";
  import { updateProjectRoute } from "@app/router";

  export let activeRoute: ProjectRoute;
@@ -14,25 +17,42 @@
  export let getImage: (path: string) => Promise<Blob>;
  export let line: string | undefined = undefined;

-
  $: lineNumber = line ? parseInt(line.substring(1)) : undefined;
-

+
  const fileExtension = blob.path.split(".").pop() ?? "";
  const lastCommit = blob.info.lastCommit;
-
  const lines = blob.binary ? 0 : (blob.content.match(/\n/g) || []).length;
-
  const hasFinalNewline = blob.content.endsWith("\n");
-
  const lineNumbers = Array(hasFinalNewline ? lines : lines + 1)
-
    .fill(0)
-
    .map((_, index) => (index + 1).toString());
  const parentDir = blob.path
    .match(/^.*\/|/)
    ?.values()
    .next().value;
+
  let content: MaybeHighlighted = undefined;
+

+
  // Any time a user clicks on a line number, the `line` prop gets updated,
+
  // and the line is highlighted, but the previous line is not unhighlighted.
+
  // So we have to make sure here that any previous highlighting gets removed,
+
  // before updating the component.
+
  beforeUpdate(() => {
+
    for (const item of document.getElementsByClassName("highlight")) {
+
      item.classList.remove("highlight");
+
    }
+
  });
+

+
  onMount(async () => {
+
    const output = await highlight(blob.content, fileExtension);
+
    if (output) {
+
      content = lineNumbersGutter(output);
+
    }
+
  });

-
  // Waiting onMount, due to the line numbers still loading.
-
  onMount(() => {
+
  afterUpdate(() => {
    if (line) {
      scrollIntoView(line);
+

+
      const element = document.getElementById(line);
+
      if (element) {
+
        element.classList.add("highlight");
+
      }
    }
  });
+

  const isMarkdown = isMarkdownPath(blob.path);
  // If we have a line number we should show the raw output.
  let showMarkdown = line ? false : isMarkdown;
@@ -40,10 +60,6 @@
    updateProjectRoute({ line: undefined });
    showMarkdown = !showMarkdown;
  };
-

-
  $: if (line) {
-
    scrollIntoView(line);
-
  }
</script>

<style>
@@ -100,29 +116,51 @@
    margin-right: 0.5rem;
  }

-
  .line-numbers {
+
  .code :global(.line-number) {
    color: var(--color-foreground-4);
-
    font-family: var(--font-family-sans-serif);
    text-align: right;
-
    user-select: none;
-
    padding: 1rem 1rem 0.5rem 1rem;
+
    padding-right: 1rem;
+
    padding-left: 1rem;
  }
-
  .line-number {
-
    display: block;
+
  .code :global(.line-number:hover) {
+
    cursor: pointer;
+
    color: var(--color-foreground);
  }
-
  .line-number:hover,
-
  .line-number.highlighted {
-
    color: var(--color-foreground-6);
+

+
  .code :global(.content) {
+
    display: inline;
+
    font-family: var(--font-family-monospace);
+
    margin: 0;
+
  }
+

+
  .code :global(.line) {
+
    line-height: 22px; /* This seems to be the line-height of a pre code block */
+
  }
+
  .code :global(.highlight) {
+
    background-color: var(--color-caution-3);
+
  }
+
  .code :global(.highlight td a) {
+
    color: var(--color-foreground);
+
  }
+

+
  .code :global(.line-content) {
+
    padding: 0;
+
    width: 100%;
  }

  .code {
-
    padding-bottom: 0.5rem;
+
    width: 100%;
+
    border-spacing: 0;
    overflow-x: auto;
+
    font-size: 1rem;
+
    padding-top: 1rem;
+
    margin-bottom: 1.5rem;
  }

  .container {
    position: relative;
    display: flex;
+
    overflow-x: auto;
    border: 1px solid var(--color-foreground-3);
    border-top-style: dashed;
    border-bottom-left-radius: var(--border-radius-small);
@@ -144,14 +182,6 @@
    margin-bottom: 1rem;
  }

-
  .highlight {
-
    position: absolute;
-
    width: 100%;
-
    height: 1.5rem;
-
    top: 1rem;
-
    background-color: var(--color-caution-3);
-
  }
-

  .no-scrollbar {
    scrollbar-width: none;
  }
@@ -165,13 +195,9 @@
  }

  @media (max-width: 960px) {
-
    .code,
-
    .line-numbers {
+
    .code {
      font-size: var(--font-size-small);
    }
-
    .highlight {
-
      display: none;
-
    }
  }

  @media (max-width: 720px) {
@@ -215,27 +241,10 @@
      </div>
    {:else if showMarkdown}
      <Readme content={blob.content} {getImage} {activeRoute} />
-
    {:else}
-
      {#if lineNumber}
-
        <div
-
          class="highlight"
-
          style="top: {lineNumber === 1 ? 1 : 1.5 * lineNumber - 0.5}rem" />
-
      {/if}
-
      <div class="line-numbers">
-
        {#each lineNumbers as lineNumber}
-
          <ProjectLink
-
            projectParams={{ line: `L${lineNumber}` }}
-
            id="L{lineNumber}">
-
            <span class="line-number" class:highlighted={lineNumber === line} />
-
            {lineNumber}
-
          </ProjectLink>
-
        {/each}
-
      </div>
-
      {#if blob.html}
-
        <pre class="code no-scrollbar">{@html blob.content}</pre>
-
      {:else}
-
        <pre class="code txt-monospace no-scrollbar">{blob.content}</pre>
-
      {/if}
+
    {:else if content}
+
      <table class="code no-scrollbar">
+
        {@html toHtml(content)}
+
      </table>
    {/if}
  </div>
</div>
modified src/base/projects/Browser.svelte
@@ -47,17 +47,8 @@
      return state.blob;
    }

-
    const isMarkdownPath = utils.isMarkdownPath(path);
    const promise =
-
      path === "/"
-
        ? project.getReadme(commit)
-
        : project.getBlob(
-
            commit,
-
            path,
-
            isMarkdownPath
-
              ? { highlight: false }
-
              : { highlight: true, theme: `base16-ocean.${theme}` },
-
          );
+
      path === "/" ? project.getReadme(commit) : project.getBlob(commit, path);

    state = { status: Status.Loading, path };
    try {
@@ -71,7 +62,7 @@
  // Get an image blob based on a relative path.
  const getImage = async (imagePath: string): Promise<proj.Blob> => {
    const finalPath = utils.canonicalize(imagePath, path);
-
    return project.getBlob(commit, finalPath, { highlight: false });
+
    return project.getBlob(commit, finalPath);
  };

  const onSelect = async (newPath: string, theme: Theme) => {
modified src/base/projects/Issue.svelte
@@ -15,7 +15,7 @@
  const getImage = async (imagePath: string): Promise<Blob> => {
    const finalPath = canonicalize(imagePath, "/"); // We only use the root path in issues.
    const commit = project.branches[project.defaultBranch]; // We suppose that all issues are only looked at on HEAD of the default branch.
-
    return project.getBlob(commit, finalPath, { highlight: false });
+
    return project.getBlob(commit, finalPath);
  };
</script>

modified src/base/projects/Patch/PatchTimeline.svelte
@@ -19,7 +19,7 @@
  const getImage = async (imagePath: string): Promise<Blob> => {
    const finalPath = canonicalize(imagePath, "/"); // We only use the root path in issues.
    const commit = project.branches[project.defaultBranch]; // We suppose that all issues are only looked at on HEAD of the default branch.
-
    return project.getBlob(commit, finalPath, { highlight: false });
+
    return project.getBlob(commit, finalPath);
  };
</script>

modified src/project.ts
@@ -278,17 +278,11 @@ export class Project implements ProjectInfo {
    ).get();
  }

-
  async getBlob(
-
    commit: string,
-
    path: string,
-
    options:
-
      | { highlight: false }
-
      | { highlight: true; theme: "base16-ocean.dark" | "base16-ocean.light" },
-
  ): Promise<Blob> {
+
  async getBlob(commit: string, path: string): Promise<Blob> {
    return new Request(
      `projects/${this.urn}/blob/${commit}/${path}`,
      this.seed.api,
-
    ).get(options);
+
    ).get();
  }

  async getReadme(commit: string): Promise<Blob> {
modified src/router/index.ts
@@ -23,7 +23,11 @@ export const base = window.HASH_ROUTING ? "./" : "/";
window.addEventListener("hashchange", e => {
  const route = pathToRoute(e.newURL);
  if (route?.resource === "projects" && route.params.hash) {
-
    updateProjectRoute({ hash: route.params.hash });
+
    if (route.params.hash.match(/^L\d+$/)) {
+
      updateProjectRoute({ line: route.params.hash });
+
    } else {
+
      updateProjectRoute({ hash: route.params.hash });
+
    }
  }
});

added src/syntax/index.ts
@@ -0,0 +1,159 @@
+
import type { Root } from "@wooorm/starry-night";
+
import type { ElementContent } from "hast";
+

+
import { createStarryNight, common } from "@wooorm/starry-night";
+
import sourceTsx from "@wooorm/starry-night/lang/source.tsx";
+
import sourceSvelte from "@wooorm/starry-night/lang/source.svelte.js";
+
import sourceSolidity from "@wooorm/starry-night/lang/source.solidity.js";
+
import sourceToml from "@wooorm/starry-night/lang/source.toml";
+
import sourceErlang from "@wooorm/starry-night/lang/source.erlang.js";
+
import sourceDockerfile from "@wooorm/starry-night/lang/source.dockerfile";
+
import sourceAsciiDoc from "@wooorm/starry-night/lang/text.html.asciidoc";
+

+
export type MaybeHighlighted = Root | undefined;
+

+
export const grammars = [
+
  ...common,
+
  sourceSvelte,
+
  sourceSolidity,
+
  sourceTsx,
+
  sourceErlang,
+
  sourceDockerfile,
+
  sourceAsciiDoc,
+
  sourceToml,
+
  // A grammar that doesn't do any parsing, but needed for files without a known filetype.
+
  {
+
    extensions: [""],
+
    names: ["raw-format"],
+
    patterns: [],
+
    scopeName: "text.raw",
+
  },
+
];
+

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

+
export async function highlight(
+
  content: string,
+
  grammar: string,
+
): Promise<MaybeHighlighted> {
+
  if (starryNight === undefined) {
+
    starryNight = await createStarryNight(grammars);
+
  }
+
  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 });
+
    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,
+
          },
+
        ],
+
      },
+
    ],
+
  };
+
}
modified tests/e2e/project.spec.ts
@@ -1,5 +1,6 @@
import type { Page } from "@playwright/test";
import { test, expect } from "@tests/support/fixtures.js";
+
import { expectUrlPersistsReload } from "@tests/support/router";

const sourceBrowsingFixture =
  "/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o";
@@ -74,9 +75,32 @@ test("source file highlighting", async ({ page }) => {
  await page.getByText("src/").click();
  await page.getByText("true.c").click();

-
  await expect(page.getByText("return")).toHaveCSS(
-
    "color",
-
    "rgb(180, 142, 173)",
+
  await expect(page.getByText("return")).toHaveCSS("color", "rgb(207, 34, 46)");
+
});
+

+
test("navigate line numbers", async ({ page }) => {
+
  await page.goto(`${sourceBrowsingFixture}/tree/main/markdown/cheatsheet.md`);
+
  await page.locator('role=button[name="Raw"]').click();
+

+
  await page.locator('[href="#L5"]').click();
+
  await expect(page.locator("#L5")).toHaveClass("line highlight");
+
  await expect(page).toHaveURL(
+
    "/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/main/markdown/cheatsheet.md#L5",
+
  );
+

+
  await expectUrlPersistsReload(page);
+
  await expect(page.locator("#L5")).toHaveClass("line highlight");
+

+
  await page.locator('[href="#L30"]').click();
+
  await expect(page.locator("#L5")).not.toHaveClass("line highlight");
+
  await expect(page.locator("#L30")).toHaveClass("line highlight");
+
  await expect(page).toHaveURL(
+
    "/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/main/markdown/cheatsheet.md#L30",
+
  );
+

+
  await page.getByText(".hidden").click();
+
  await expect(page).toHaveURL(
+
    "/seeds/0.0.0.0/rad:git:hnrkgd7sjt79k4j59ddh11ooxg18rk7soej8o/tree/main/.hidden",
  );
});

modified tests/e2e/settings.spec.ts
@@ -44,7 +44,7 @@ test("change theme", async ({ page }) => {
    "rgb(243, 246, 253)",
  );
  // Source highlighting reacts to theme change.
-
  await expect(page.getByText("() {")).toHaveCSS("color", "rgb(79, 91, 102)");
+
  await expect(page.getByText("() {")).toHaveCSS("color", "rgb(26, 26, 44)");

  await page.locator(".theme .toggle").click();
  await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
@@ -53,7 +53,7 @@ test("change theme", async ({ page }) => {
    "rgb(11, 19, 26)",
  );
  // Source highlighting reacts to theme change.
-
  await expect(page.getByText("() {")).toHaveCSS("color", "rgb(192, 197, 206)");
+
  await expect(page.getByText("() {")).toHaveCSS("color", "rgb(255, 255, 255)");
});

test("change code font", async ({ page }) => {
modified tests/support/fixtures.ts
@@ -83,6 +83,13 @@ export const test = base.extend<{
            status: 200,
            path: "./public/favicon.ico",
          });
+
        } else if (
+
          route.request().url().startsWith("https://esm.sh/vscode-oniguruma@1")
+
        ) {
+
          route.fulfill({
+
            status: 200,
+
            path: "./tests/support/onig.wasm",
+
          });
        } else {
          log(
            `Aborted remote request: ${route.request().url()}`,
added tests/support/onig.wasm
modified vite.config.ts
@@ -70,6 +70,22 @@ export default defineConfig({
          ],
          cache: ["lru-cache", "@stardazed/streams"],
          markdown: ["katex", "dompurify", "marked", "@radicle/gray-matter"],
+
          syntax: ["@wooorm/starry-night"],
+
          grammarsTsx: [
+
            "@wooorm/starry-night/lang/source.ts.js",
+
            "@wooorm/starry-night/lang/source.tsx.js",
+
          ],
+
          grammars: [
+
            "@wooorm/starry-night/lang/source.python.js",
+
            "@wooorm/starry-night/lang/source.js.js",
+
            "@wooorm/starry-night/lang/source.perl.js",
+
            "@wooorm/starry-night/lang/source.haskell.js",
+
            "@wooorm/starry-night/lang/source.ruby.js",
+
            "@wooorm/starry-night/lang/source.css.js",
+
            "@wooorm/starry-night/lang/source.solidity.js",
+
            "@wooorm/starry-night/lang/source.cs.js",
+
            "@wooorm/starry-night/lang/source.swift.js",
+
          ],
          dom: ["svelte", "pure-svg-code", "twemoji"],
        },
      },