Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions apps/docs/__tests__/ContentHighlighting_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
open Vitest

type registeredLanguage
type highlightOptions = {language: string}
type highlightResult = {value: string}

@module("highlight.js/lib/core") @scope("default")
external createHighlighter: unit => HighlightLanguages.highlighter = "newInstance"

@send
external getLanguage: (HighlightLanguages.highlighter, string) => option<registeredLanguage> =
"getLanguage"

@send
external highlight: (HighlightLanguages.highlighter, string, highlightOptions) => highlightResult =
"highlight"

let aliases = [
"rescript",
"res",
"resi",
"javascript",
"js",
"jsx",
"mjs",
"cjs",
"css",
"ts",
"tsx",
"mts",
"cts",
"typescript",
"sh",
"bash",
"zsh",
"toml",
"json",
"jsonc",
"text",
"txt",
"html",
"xhtml",
"rss",
"atom",
"xjb",
"xsd",
"xsl",
"plist",
"wsf",
"svg",
"diff",
"patch",
"yaml",
"yml",
]

test("content registration preserves every established language name and alias", async () => {
let highlighter = createHighlighter()
highlighter->ContentHighlighting.register

let missingAliases =
aliases->Array.filter(alias => highlighter->getLanguage(alias)->Option.isNone)
expect(missingAliases)->toEqual([])
})

test("content registration produces highlighted ReScript and escaped plain text", async () => {
let highlighter = createHighlighter()
highlighter->ContentHighlighting.register

let rescript = highlighter->highlight("let count = 1", {language: "res"})
let text = highlighter->highlight("<button>Example & code</button>", {language: "text"})

expect(rescript.value->String.includes("<span class=\"hljs-keyword\">let</span>"))->toBe(true)
expect(rescript.value->String.includes("<span class=\"hljs-number\">1</span>"))->toBe(true)
expect(text.value)->toBe("&lt;button&gt;Example &amp; code&lt;/button&gt;")
})

test("repeated content initialization leaves registered grammars unchanged", async () => {
let highlighter = createHighlighter()
highlighter->ContentHighlighting.register
let original = highlighter->getLanguage("javascript")
let before = highlighter->highlight("const count: number = 1;", {language: "ts"})

highlighter->ContentHighlighting.register

let after = highlighter->highlight("const count: number = 1;", {language: "ts"})
expect(highlighter->getLanguage("javascript"))->toBe(original)
expect(highlighter->getLanguage("js"))->toBe(original)
expect(after.value)->toBe(before.value)
})

test("content registration highlights YAML installation examples and their alias", async () => {
let highlighter = createHighlighter()
highlighter->ContentHighlighting.register
let code = "packages:\n - apps/*"

let yaml = highlighter->highlight(code, {language: "yaml"})
let alias = highlighter->highlight(code, {language: "yml"})

expect(yaml.value->String.includes("<span class=\"hljs-attr\">packages:</span>"))->toBe(true)
expect(alias.value)->toBe(yaml.value)
})
51 changes: 51 additions & 0 deletions apps/docs/__tests__/HighlightLanguages_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
open Vitest

type registeredLanguage
type highlightOptions = {language: string}
type highlightResult = {value: string}

@module("highlight.js/lib/core") @scope("default")
external createHighlighter: unit => HighlightLanguages.highlighter = "newInstance"

@module("highlight.js/lib/languages/javascript")
external javascript: HighlightLanguages.definition = "default"

@module("highlight.js/lib/languages/plaintext")
external plaintext: HighlightLanguages.definition = "default"

@send
external getLanguage: (HighlightLanguages.highlighter, string) => option<registeredLanguage> =
"getLanguage"

@send
external highlight: (HighlightLanguages.highlighter, string, highlightOptions) => highlightResult =
"highlight"

test("registering a missing language makes its grammar and aliases available", async () => {
let highlighter = createHighlighter()

expect(highlighter->getLanguage("javascript"))->toEqual(None)
expect(highlighter->getLanguage("js"))->toEqual(None)
highlighter->HighlightLanguages.ensureRegistered("javascript", javascript)

let result = highlighter->highlight("const answer = 42;", {language: "js"})
expect(result.value)->toBe(
"<span class=\"hljs-keyword\">const</span> answer = <span class=\"hljs-number\">42</span>;",
)
})

test("registering an existing name or alias does not replace its grammar", async () => {
let highlighter = createHighlighter()
highlighter->HighlightLanguages.ensureRegistered("javascript", javascript)
let original = highlighter->getLanguage("javascript")

highlighter->HighlightLanguages.ensureRegistered("javascript", plaintext)
highlighter->HighlightLanguages.ensureRegistered("js", plaintext)

expect(highlighter->getLanguage("javascript"))->toBe(original)
expect(highlighter->getLanguage("js"))->toBe(original)
let result = highlighter->highlight("const answer = 42;", {language: "js"})
expect(result.value)->toBe(
"<span class=\"hljs-keyword\">const</span> answer = <span class=\"hljs-number\">42</span>;",
)
})
35 changes: 35 additions & 0 deletions apps/docs/__tests__/JavaScriptHighlighting_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
open Vitest

type registeredLanguage
type highlightOptions = {language: string}
type highlightResult = {value: string}

@module("highlight.js/lib/core") @scope("default")
external createHighlighter: unit => HighlightLanguages.highlighter = "newInstance"

@send
external listLanguages: HighlightLanguages.highlighter => array<string> = "listLanguages"

@send
external getLanguage: (HighlightLanguages.highlighter, string) => option<registeredLanguage> =
"getLanguage"

@send
external highlight: (HighlightLanguages.highlighter, string, highlightOptions) => highlightResult =
"highlight"

test("playground registration loads only JavaScript and its working aliases", async () => {
let highlighter = createHighlighter()
highlighter->JavaScriptHighlighting.register

let missingAliases =
["javascript", "js", "jsx", "mjs", "cjs"]->Array.filter(alias =>
highlighter->getLanguage(alias)->Option.isNone
)
expect(highlighter->listLanguages)->toEqual(["javascript"])
expect(missingAliases)->toEqual([])
let result = highlighter->highlight("const answer = 42;", {language: "js"})
expect(result.value)->toBe(
"<span class=\"hljs-keyword\">const</span> answer = <span class=\"hljs-number\">42</span>;",
)
})
29 changes: 0 additions & 29 deletions apps/docs/app/DocsRoot.res
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,6 @@ external mainCss: string = "default"
@module("../styles/_hljs.css?url")
external hljsCss: string = "default"

%%raw(`
import hljs from 'highlight.js/lib/core';
import bash from 'highlight.js/lib/languages/bash';
import css from 'highlight.js/lib/languages/css';
import diff from 'highlight.js/lib/languages/diff';
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';
import json from 'highlight.js/lib/languages/json';
import text from 'highlight.js/lib/languages/plaintext';
import html from 'highlight.js/lib/languages/xml';
import yaml from 'highlight.js/lib/languages/yaml';
import toml from 'highlight.js/lib/languages/ini';
import rescript from 'highlightjs-rescript';

hljs.registerLanguage('rescript', rescript)
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('css', css)
hljs.registerLanguage('ts', typescript)
hljs.registerLanguage('sh', bash)
hljs.registerLanguage('bash', bash)
hljs.registerLanguage('toml', toml)
hljs.registerLanguage('json', json)
hljs.registerLanguage('text', text)
hljs.registerLanguage('html', html)
hljs.registerLanguage('diff', diff)
hljs.registerLanguage('typescript', typescript)
hljs.registerLanguage('yaml', yaml)
`)

open ReactRouter

@react.component
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/app/DocsRoot.resi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** Shared shell styles and the stable Cypress bootstrap slot.
Feature-specific styles load with their components. */
/** Shared shell, token styles, and the stable Cypress bootstrap slot.
Runtime grammars belong to content routes and the playground. */
@react.component
let default: unit => Jsx.element
51 changes: 30 additions & 21 deletions apps/docs/app/DocsRoutes.res
Original file line number Diff line number Diff line change
Expand Up @@ -74,30 +74,39 @@ let syntaxLookupDetailRoutes =

let default = [
index("./routes/LandingPageRoute.jsx"),
route("packages", "./routes/PackagesRoute.jsx"),
route("try", "./routes/TryRoute.jsx"),
route("brand", "./routes/BrandRoute.jsx"),
route("blog", "./routes/BlogRoute.jsx", ~options={id: "blog-index"}),
route("blog/archived", "./routes/BlogRoute.jsx", ~options={id: "blog-archived"}),
...blogArticleRoutes,
...communityRoutes,
route("docs", "./routes/DocsOverview.jsx", ~options={id: "docs-overview"}),
layout(
"./layouts/DocsLayoutRoute.jsx",
"./layouts/ContentLayoutRoute.jsx",
[
route("docs/manual/api", "./routes/ApiOverviewRoute.jsx", ~options={id: "api-overview"}),
route("docs/manual/api/stdlib", "./routes/ApiRoute.jsx", ~options={id: "api-stdlib"}),
route("docs/manual/api/introduction", "./routes/ApiRoute.jsx", ~options={id: "api-intro"}),
route("docs/manual/api/belt", "./routes/ApiRoute.jsx", ~options={id: "api-belt"}),
route("docs/manual/api/dom", "./routes/ApiRoute.jsx", ~options={id: "api-dom"}),
...stdlibRoutes,
...beltRoutes,
...domRoutes,
...docsManualRoutes,
...docsGuidesRoutes,
...docsReactRoutes,
route("syntax-lookup", "./routes/SyntaxLookupRoute.jsx", ~options={id: "syntax-lookup"}),
...syntaxLookupDetailRoutes,
route("packages", "./routes/PackagesRoute.jsx"),
route("brand", "./routes/BrandRoute.jsx"),
route("blog", "./routes/BlogRoute.jsx", ~options={id: "blog-index"}),
route("blog/archived", "./routes/BlogRoute.jsx", ~options={id: "blog-archived"}),
...blogArticleRoutes,
...communityRoutes,
route("docs", "./routes/DocsOverview.jsx", ~options={id: "docs-overview"}),
layout(
"./layouts/DocsLayoutRoute.jsx",
[
route("docs/manual/api", "./routes/ApiOverviewRoute.jsx", ~options={id: "api-overview"}),
route("docs/manual/api/stdlib", "./routes/ApiRoute.jsx", ~options={id: "api-stdlib"}),
route(
"docs/manual/api/introduction",
"./routes/ApiRoute.jsx",
~options={id: "api-intro"},
),
route("docs/manual/api/belt", "./routes/ApiRoute.jsx", ~options={id: "api-belt"}),
route("docs/manual/api/dom", "./routes/ApiRoute.jsx", ~options={id: "api-dom"}),
...stdlibRoutes,
...beltRoutes,
...domRoutes,
...docsManualRoutes,
...docsGuidesRoutes,
...docsReactRoutes,
route("syntax-lookup", "./routes/SyntaxLookupRoute.jsx", ~options={id: "syntax-lookup"}),
...syntaxLookupDetailRoutes,
],
),
],
),
route("*", "./routes/NotFoundRoute.jsx"),
Expand Down
1 change: 1 addition & 0 deletions apps/docs/app/DocsRoutes.resi
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ let docsReactRoutes: array<ReactRouter.Routes.t>
let docsGuidesRoutes: array<ReactRouter.Routes.t>
let communityRoutes: array<ReactRouter.Routes.t>
let syntaxLookupDetailRoutes: array<ReactRouter.Routes.t>
/** Keep home, the lazy playground, and not-found outside the content highlighting boundary. */
let default: array<ReactRouter.Routes.t>
5 changes: 5 additions & 0 deletions apps/docs/app/layouts/ContentLayoutRoute.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Route-module initialization runs before any content children render.
let () = ContentHighlighting.register(HighlightLanguages.defaultInstance)

@react.component
let default = () => <ReactRouter.Outlet />
3 changes: 3 additions & 0 deletions apps/docs/app/layouts/ContentLayoutRoute.resi
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/** Own runtime highlighting for content routes without adding a DOM wrapper. */
@react.component
let default: unit => React.element
7 changes: 7 additions & 0 deletions apps/docs/e2e/Playground.cy.res
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ describe("Playground", () => {
waitForHydration()
})

it("should highlight JavaScript after a direct playground load", () => {
visit("/try")
get(".cm-editor")->shouldBeVisible->ignore
get("pre.whitespace-pre-wrap")->shouldContainText("react/jsx-runtime")->ignore
get("pre code.lang-js span[class^='hljs-']")->should("exist")->ignore
})

it("should compile and run imported code in the playground", () => {
// Navigate to the playground from the homepage
clickNavLink(~testId="navbar-primary-left-content", ~text="Playground")
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/e2e/bindings/Cypress.res
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ external shouldCssProperty: (chain<elements>, @as("have.css") _, string) => chai
@send external shouldInt: (chain<'a>, string, int) => chain<'a> = "should"
@send
external shouldValue: (chain<elements>, @as("have.value") _, string) => chain<elements> = "should"
@send
external shouldText: (chain<elements>, @as("have.text") _, string) => chain<elements> = "should"
@send external shouldEqual: (chain<'a>, @as("equal") _, 'a) => chain<'a> = "should"
@send external shouldDeepEqual: (chain<'a>, @as("deep.equal") _, 'a) => chain<'a> = "should"
@send external shouldMatch: (chain<'a>, @as("match") _, RegExp.t) => chain<'a> = "should"
Expand All @@ -95,6 +97,7 @@ external shouldProperty: (chain<elements>, @as("have.prop") _, string, string) =
@send external propertyInt: (chain<'a>, string) => chain<int> = "its"
@send external shouldSatisfy: (chain<'a>, 'a => unit) => chain<'a> = "should"
@send external click: chain<elements> => chain<elements> = "click"
@send external first: chain<elements> => chain<elements> = "first"
@send external typeText: (chain<elements>, string) => chain<elements> = "type"
@send
external containsChildRegex: (chain<elements>, string, RegExp.t) => chain<elements> = "contains"
Expand Down
33 changes: 32 additions & 1 deletion apps/docs/e2e/homepage/HomepageHighlighting.cy.res
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
open Cypress
open HomepageHelpers

it("initial homepage scripts do not include example preparation or compression", () => {
it("initial homepage scripts exclude example preparation and the syntax runtime", () => {
homepageDocument(document => {
let scripts = document->initialScriptUrls
expect(scripts->Array.length)->greaterThan(0)
Expand All @@ -12,6 +12,7 @@ it("initial homepage scripts do not include example preparation or compression",
response => {
expect(response.status, ~message=asset)->equal(200)
expect(response.body, ~message=asset)->notInclude("compressToEncodedURIComponent")
expect(response.body, ~message=asset)->notInclude("registerLanguage")
expect(response.body, ~message=asset)->notInclude("function Playground$Button(props)")
},
)
Expand All @@ -20,6 +21,36 @@ it("initial homepage scripts do not include example preparation or compression",
})
})

it("content routes highlight JSON on direct loads and cold homepage navigation", () => {
let path = "/docs/manual/build-configuration"
request(`${path}/`)
->then(response => {
expect(response.status)->equal(200)
let document = parser()->parseHtml(response.body)
let text =
document
->querySelector("code.lang-json .hljs-attr")
->Null.toOption
->Option.flatMap(element => element->textContent->Null.toOption)
expect(text)->equal(Some(`"sources"`))
})
->ignore
visit(`${path}/`)
get("code.lang-json .hljs-attr")->first->shouldText(`"sources"`)->ignore
visit("/")
cyLocation("pathname")->shouldEqual("/")->ignore
containsIn("h1", headline)->should("be.visible")->ignore
containsInRegex("a", /^Docs$/)->click->ignore
containsInRegex("h1", /^ReScript$/)->should("be.visible")->ignore
get("aside:visible")
->containsChildRegex("a", /^Configuration$/)
->scrollIntoView
->click
->ignore
cyLocation("pathname")->shouldEqual(path)->ignore
get("code.lang-json .hljs-attr")->first->shouldText(`"sources"`)->ignore
})

it("prepared examples survive hydration and navigation back from documentation", () => {
homepageDocument(document => {
let examples = ["res", "js"]->Array.map(
Expand Down
Loading
Loading