diff --git a/apps/docs/__tests__/LandingPageBehavior_.test.res b/apps/docs/__tests__/LandingPageBehavior_.test.res index cbe53431c..b21e3b026 100644 --- a/apps/docs/__tests__/LandingPageBehavior_.test.res +++ b/apps/docs/__tests__/LandingPageBehavior_.test.res @@ -29,7 +29,7 @@ let getOpenGraphImageUrl = () => { test("landing page Open Graph image targets its absolute page URL", async () => { let _screen = await render( - + , ) let pageUrl = WebAPI.URL.make(~url=Env.root_url) @@ -52,7 +52,7 @@ test( async () => { let screen = await render( - + , ) @@ -89,7 +89,7 @@ test( test("landing page playground hero renders highlighted code tokens", async () => { let screen = await render( - + , ) @@ -120,7 +120,7 @@ test( let screen = await render( - + , ) diff --git a/apps/docs/__tests__/visual/LandingPage_.test.res b/apps/docs/__tests__/visual/LandingPage_.test.res index fd8d8aabc..81cef0e5c 100644 --- a/apps/docs/__tests__/visual/LandingPage_.test.res +++ b/apps/docs/__tests__/visual/LandingPage_.test.res @@ -6,7 +6,7 @@ let snapshotSection = async (~width, ~height, ~sectionTestId, ~screenshotName) = let screen = await render( - + , ) diff --git a/apps/docs/app/routes/LandingPage.res b/apps/docs/app/routes/LandingPage.res index aab1d4bbc..50ad0b9ee 100644 --- a/apps/docs/app/routes/LandingPage.res +++ b/apps/docs/app/routes/LandingPage.res @@ -34,15 +34,18 @@ module Intro = { } } -module PlaygroundHero = { - type example = { - res: string, - js: string, - } +type example = { + res: string, + js: string, +} - let examples = [ - { - res: `module Button = { +type highlightedExample = { + res: string, + js: string, +} + +let playgroundExample: example = { + res: `module Button = { @react.component let make = (~count) => { let times = switch count { @@ -55,7 +58,7 @@ module PlaygroundHero = { } }`, - js: `import * as JsxRuntime from "react/jsx-runtime"; + js: `import * as JsxRuntime from "react/jsx-runtime"; function Playground$Button(props) { let count = props.count; @@ -75,13 +78,11 @@ let Button = { export { Button, }`, - }, - ] +} +module PlaygroundHero = { @react.component - let make = () => { - let (example, _setExample) = React.useState(_ => examples->Array.getUnsafe(0)) - + let make = (~highlightedExample: highlightedExample) => { //Playground Section & Background
@@ -98,7 +99,10 @@ export { {React.string("Write in ReScript")}
-              {HighlightJs.renderHLJS(~darkmode=true, ~code=example.res, ~lang="res", ())}
+              
             
//Right Side (JavaScript) @@ -109,14 +113,17 @@ export { {React.string("Compile to JavaScript")}
-              {HighlightJs.renderHLJS(~darkmode=true, ~code=example.js, ~lang="js", ())}
+              
             
/* ---Link to Playground--- */ {React.string("Edit this example in Playground")} @@ -683,7 +690,7 @@ module CuratedResources = { } @react.component -let make = (~components=MarkdownComponents.default) => { +let make = (~components=MarkdownComponents.default, ~highlightedExample: highlightedExample) => { <> {
- + diff --git a/apps/docs/app/routes/LandingPage.resi b/apps/docs/app/routes/LandingPage.resi index 65f28d025..fef9f9086 100644 --- a/apps/docs/app/routes/LandingPage.resi +++ b/apps/docs/app/routes/LandingPage.resi @@ -1,2 +1,17 @@ +type example = { + res: string, + js: string, +} + +type highlightedExample = { + res: string, + js: string, +} + +let playgroundExample: example + @react.component -let make: (~components: MarkdownComponents.t=?) => React.element +let make: ( + ~components: MarkdownComponents.t=?, + ~highlightedExample: highlightedExample, +) => React.element diff --git a/apps/docs/app/routes/LandingPageRoute.res b/apps/docs/app/routes/LandingPageRoute.res index bf5446708..c96812b71 100644 --- a/apps/docs/app/routes/LandingPageRoute.res +++ b/apps/docs/app/routes/LandingPageRoute.res @@ -1,5 +1,17 @@ +type loaderData = LandingPage.highlightedExample + +let loader: ReactRouter.Loader.t = async _ => { + let {res, js} = LandingPage.playgroundExample + let highlightedExample: LandingPage.highlightedExample = { + res: ShikiHighlighter.highlight(~code=res, ~language="rescript"), + js: ShikiHighlighter.highlight(~code=js, ~language="javascript"), + } + highlightedExample +} + let default = () => { + let highlightedExample = ReactRouter.useLoaderData() <> - + } diff --git a/apps/docs/app/routes/LandingPageRoute.resi b/apps/docs/app/routes/LandingPageRoute.resi index 5f02c123c..caa64bfd9 100644 --- a/apps/docs/app/routes/LandingPageRoute.resi +++ b/apps/docs/app/routes/LandingPageRoute.resi @@ -1 +1,5 @@ +type loaderData = LandingPage.highlightedExample + +let loader: ReactRouter.Loader.t + let default: unit => React.element diff --git a/apps/docs/package.json b/apps/docs/package.json index 3baad8db8..69da05e2c 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -68,6 +68,7 @@ "remark-gfm": "^4.0.1", "remark-validate-links": "^13.1.0", "rescript": "^12.2.0", + "shiki": "^4.4.3", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "vfile-matter": "^5.0.1" diff --git a/apps/docs/src/bindings/ShikiHighlighter.res b/apps/docs/src/bindings/ShikiHighlighter.res new file mode 100644 index 000000000..6665fb376 --- /dev/null +++ b/apps/docs/src/bindings/ShikiHighlighter.res @@ -0,0 +1,2 @@ +@module("../server/ShikiHighlighter.js") +external highlight: (~code: string, ~language: string) => string = "highlight" diff --git a/apps/docs/src/server/ShikiHighlighter.js b/apps/docs/src/server/ShikiHighlighter.js new file mode 100644 index 000000000..f0280365f --- /dev/null +++ b/apps/docs/src/server/ShikiHighlighter.js @@ -0,0 +1,32 @@ +import { createHighlighterCoreSync, hastToHtml } from "shiki/core"; +import { createJavaScriptRegexEngine } from "shiki/engine/javascript"; +import javascript from "@shikijs/langs/javascript"; +import githubDark from "@shikijs/themes/github-dark"; + +// Vendored from rescript-vscode at commit 76d30468cb05401c10dd1e9dcf5a2993f8fc7140. +// https://github.com/rescript-lang/rescript-vscode/blob/76d30468cb05401c10dd1e9dcf5a2993f8fc7140/grammars/rescript.tmLanguage.json +import rescriptGrammar from "./rescript.tmLanguage.json" with { type: "json" }; + +const highlighter = createHighlighterCoreSync({ + engine: createJavaScriptRegexEngine(), + langs: [ + javascript, + { + ...rescriptGrammar, + name: "rescript", + aliases: ["res"], + }, + ], + themes: [githubDark], +}); + +export const highlight = (code, language) => { + const tree = highlighter.codeToHast(code, { + lang: language, + theme: "github-dark", + }); + const pre = tree.children[0]; + const codeElement = pre.children[0]; + + return codeElement.children.map((child) => hastToHtml(child)).join(""); +}; diff --git a/apps/docs/src/server/rescript.tmLanguage.json b/apps/docs/src/server/rescript.tmLanguage.json new file mode 100644 index 000000000..7a5bade0f --- /dev/null +++ b/apps/docs/src/server/rescript.tmLanguage.json @@ -0,0 +1,563 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "ReScript", + "scopeName": "source.rescript", + "repository": { + "RE_KEYWORD_CONTROL": { + "name": "keyword.control", + "match": "\\b(and|as|assert|async|await|catch|constraint|downto|else|exception|external|for|if|in|lazy|mutable|rec|switch|to|try|when|while|with|private)\\b" + }, + "RE_TO_DOWNTO_AS_LABELS": { + "patterns": [ + { + "match": "~(to|downto)\\s*(=)", + "captures": { + "1": { + "name": "variable" + }, + "2": { + "name": "keyword.operator keyword" + } + } + }, + { + "match": "~(to|downto)\\s+(as)", + "captures": { + "1": { + "name": "variable" + }, + "2": { + "name": "keyword.control" + } + } + } + ] + }, + "RE_CONSTANTS_BOOL": { + "name": "constant.language.boolean", + "match": "\\b(false|true)\\b" + }, + "RE_KEYWORD": { + "name": "storage.type", + "match": "\\b(include|let|module|of|open|type)\\b" + }, + "commentLine": { + "match": "//.*", + "name": "comment.line" + }, + "commentBlock": { + "name": "comment.block", + "begin": "/\\*", + "end": "\\*/", + "patterns": [ + { + "include": "#commentBlock" + } + ] + }, + "punctuation": { + "patterns": [ + { + "match": "~", + "name": "punctuation.definition.keyword" + }, + { + "match": ";", + "name": "punctuation.terminator" + }, + { + "match": "\\.", + "name": "punctuation.accessor" + }, + { + "match": "\\,", + "name": "punctuation.separator" + }, + { + "match": "\\?|:", + "name": "punctuation.separator" + }, + { + "match": "\\|(?!\\|)", + "name": "punctuation.separator" + }, + { + "match": "\\{", + "name": "punctuation.section.braces.begin" + }, + { + "match": "\\}", + "name": "punctuation.section.braces.end" + }, + { + "match": "\\[", + "name": "punctuation.section.brackets.begin" + }, + { + "match": "\\]", + "name": "punctuation.section.brackets.end" + }, + { + "match": "\\(", + "name": "punctuation.section.parens.begin" + }, + { + "match": "\\)", + "name": "punctuation.section.parens.end" + } + ] + }, + "keyword": { + "patterns": [ + { + "include": "#RE_TO_DOWNTO_AS_LABELS" + }, + { + "include": "#RE_KEYWORD_CONTROL" + }, + { + "include": "#RE_KEYWORD" + } + ] + }, + "constant": { + "patterns": [ + { + "include": "#RE_CONSTANTS_BOOL" + } + ] + }, + "string-character-escape": { + "name": "constant.character.escape", + "match": "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|u{[0-9A-Fa-f]+}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" + }, + "string": { + "patterns": [ + { + "name": "string.quoted.double", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "1": { + "name": "punctuation.definition.string.begin" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end" + } + }, + "patterns": [ + { + "include": "#string-character-escape" + } + ] + }, + { + "name": "string.template", + "begin": "([a-z_][0-9a-zA-Z_]*)?(`)", + "end": "(?", + "name": "storage.type.function keyword.declaration.function" + } + ] + }, + "character": { + "patterns": [ + { + "match": "'[\\x00-\\x7F]'", + "name": "string.quoted.single" + } + ] + }, + "typeParameter": { + "patterns": [ + { + "match": "'[A-Za-z][A-Za-z0-9_]*", + "name": "support.type" + } + ] + }, + "defaultIdIsVariable": { + "patterns": [ + { + "match": "[A-Za-z_][A-Za-z0-9_]*", + "name": "variable" + } + ] + }, + "number": { + "patterns": [ + { + "match": "\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]+)?([eE][-+]?[0-9_]+)?)?)\\b", + "name": "constant.numeric" + } + ] + }, + "operator": { + "patterns": [ + { + "match": "->|\\|\\||&&|\\+\\+|\\*\\*|\\+\\.|\\+|-\\.|-|\\*\\.|\\*|/\\.|/|\\.\\.\\.|\\.\\.|===|==|\\^|:=|!|>=(?! *\\?)|<=|=", + "name": "keyword.operator" + }, + { + "match": "\\|>", + "name": "invalid.deprecated" + } + ] + }, + "constructor": { + "patterns": [ + { + "match": "\\b[A-Z][0-9a-zA-Z_]*\\b", + "name": "variable.other.enummember" + }, + { + "match": "(#)\\s*([a-zA-Z][0-9a-zA-Z_]*)\\b", + "captures": { + "1": { + "name": "variable.other.enummember" + }, + "2": { + "name": "variable.other.enummember" + } + } + }, + { + "match": "(#)\\s*(\\.\\.\\.)\\b", + "captures": { + "1": { + "name": "variable.other.enummember" + }, + "2": { + "name": "variable.other.enummember" + } + } + }, + { + "match": "(#)", + "captures": { + "1": { + "name": "variable.other.enummember" + } + } + } + ] + }, + "entity-literal": { + "patterns": [ + { + "match": "\\b(list|dict)(\\{)", + "captures": { + "1": { + "name": "keyword" + }, + "2": { + "name": "punctuation.section.braces.begin" + } + } + }, + { + "match": "\\}", + "name": "punctuation.section.braces.end" + } + ] + }, + "attribute": { + "patterns": [ + { + "match": "(%%?|@@?)([A-Za-z_][A-Za-z0-9_\\.]*)", + "captures": { + "1": { + "name": "punctuation.decorator" + }, + "2": { + "patterns": [ + { + "match": "bs\\.send\\.pipe", + "name": "invalid.deprecated" + }, + { + "match": "splice", + "name": "invalid.illegal" + }, + { + "match": "(bs\\.)?([A-Za-z_][A-Za-z0-9_\\.]*)", + "captures": { + "1": { + "name": "invalid.deprecated" + }, + "2": { + "name": "entity.name.function" + } + } + }, + { + "match": "[A-Za-z_][A-Za-z0-9_\\.]*", + "name": "entity.name.function" + } + ] + } + } + } + ] + }, + "jsx": { + "patterns": [ + { + "match": "<>||", + "name": "punctuation.definition.tag" + }, + { + "match": "