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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Add: [OCaml](snippets/ocaml) syntax highlighting of module paths like `My_module.my_function` and function definition names
* Add: More legible class name highlight color in light mode
2 changes: 1 addition & 1 deletion znai-reactjs/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@
--znai-open-api-sub-header-color: var(--znai-light-black);

--znai-default-code-block-color: var(--znai-color-black);
--znai-code-class-color: #39397d;
--znai-code-class-color: #6f42c1;
--znai-code-constant-color: #c58fe0;
--znai-code-function-color: #a66f2b;
--znai-code-keyword-color: #038091;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function syntaxHighlightSnippetDemo(registry) {
.add('graphql', () => <Snippet snippet={graphqlCode()} lang="graphql" highlight={[5]}/>)
.add('cypher', () => <Snippet snippet={cypherCode()} lang="cyp" highlight={[4]}/>)
.add('sql', () => <Snippet snippet={sqlCode()} lang="sql" highlight={[1]}/>)
.add('ocaml', () => <Snippet snippet={ocamlCode()} lang="ocaml"/>)
.add('diff', () => <Snippet snippet={diffCode()} lang="diff"/>)
.add('diff-javascript', () => <Snippet snippet={diffJavaScriptCode()} lang="diff-javascript"/>)
}
Expand All @@ -44,6 +45,21 @@ function sqlCode() {
"WHERE condition;"
}

function ocamlCode() {
return '(* circle area calculation *)\n' +
'open Base\n' +
'\n' +
'module Circle = struct\n' +
' let pi = 3.14159\n' +
' let area radius = pi *. radius *. radius\n' +
'end\n' +
'\n' +
'let () =\n' +
' let area = Circle.area 5.0 in\n' +
' Stdio.printf "area: %f\\n" area;\n' +
' List.iter [ 1; 2; 3 ] ~f:(fun x -> Stdio.printf "%d\\n" x)'
}

function yamlCode() {
return 'invoice: 34843\n' +
'date : 2001-01-23\n' +
Expand Down
2 changes: 1 addition & 1 deletion znai-reactjs/src/doc-elements/code-snippets/codeParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import 'prismjs/components/prism-diff'
import 'prismjs/components/prism-graphql'
import 'prismjs/components/prism-cypher'
import 'prismjs/components/prism-sql'
import 'prismjs/components/prism-ocaml'
import './prismOcaml'
import 'prismjs/plugins/autoloader/prism-autoloader'
import 'prismjs/plugins/diff-highlight/prism-diff-highlight'

Expand Down
74 changes: 74 additions & 0 deletions znai-reactjs/src/doc-elements/code-snippets/codeUtils.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,80 @@ describe("codeUtils", () => {
])
});

it("should tokenize ocaml module path into module and function parts", () => {
const tokens = parseCode("ocaml", "let () = My_module.my_function 42")

expect(tokens).toEqual([
{ type: 'keyword', content: 'let' },
' ',
{ type: 'punctuation', content: '(' },
{ type: 'punctuation', content: ')' },
' ',
{ type: 'operator', content: '=' },
' ',
{ type: 'class-name', content: 'My_module' },
{ type: 'punctuation', content: '.' },
{ type: 'function', content: 'my_function' },
' ',
{ type: 'number', content: '42' }
])
});

it("should tokenize ocaml nested module path", () => {
const tokens = parseCode("ocaml", "Foo.Bar.baz")

expect(tokens).toEqual([
{ type: 'class-name', content: 'Foo' },
{ type: 'punctuation', content: '.' },
{ type: 'class-name', content: 'Bar' },
{ type: 'punctuation', content: '.' },
{ type: 'function', content: 'baz' }
])
});

it("should tokenize ocaml module name after open and module keywords", () => {
const openTokens = parseCode("ocaml", "open My_module")
expect(openTokens).toEqual([
{ type: 'keyword', content: 'open' },
' ',
{ type: 'class-name', content: 'My_module' }
])

const moduleTokens = parseCode("ocaml", "module Circle = struct")
expect(moduleTokens).toEqual([
{ type: 'keyword', content: 'module' },
' ',
{ type: 'class-name', content: 'Circle' },
' ',
{ type: 'operator', content: '=' },
' ',
{ type: 'keyword', content: 'struct' }
])
});

it("should tokenize ocaml function definition name but not value binding name", () => {
const functionTokens = parseCode("ocaml", "let area radius = radius *. radius")
expect(functionTokens).toEqual([
{ type: 'keyword', content: 'let' },
' ',
{ type: 'function', content: 'area' },
' radius ',
{ type: 'operator', content: '=' },
' radius ',
{ type: 'operator', content: '*.' },
' radius'
])

const valueTokens = parseCode("ocaml", "let pi = 3.14159")
expect(valueTokens).toEqual([
{ type: 'keyword', content: 'let' },
' pi ',
{ type: 'operator', content: '=' },
' ',
{ type: 'number', content: '3.14159' }
])
});

it("splits java multi line comment into separate lines", () => {
const tokens = parseCode('java', ` /** hello
multi line
Expand Down
54 changes: 54 additions & 0 deletions znai-reactjs/src/doc-elements/code-snippets/prismOcaml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2026 znai maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as Prism from 'prismjs'

import 'prismjs/components/prism-ocaml'

// prism ocaml grammar leaves module paths like My_module.my_function untokenized.
// add rules to render module names and functions in different colors.
// note: rule names must match tokens.css classes directly since normalizeToken in codeParser drops prism aliases.
// order matters: "function" needs the full "My_module.my_function" text present for its lookbehind,
// so it must run before "class-name" consumes the module part.
// insert before "label" so that in definitions like: let g ~label = ...
// the parameters are still present for the function definition lookahead.
Prism.languages.insertBefore('ocaml', 'label', {
'function': [
{
// lowercase identifier accessed through a module path, e.g. my_function in My_module.my_function
pattern: /(\b[A-Z][\w']*\s*\.\s*)[a-z_][\w']*/,
lookbehind: true
},
{
// function definition name followed by parameters, e.g. area in: let area radius = ...
// value bindings like: let pi = 3.14 are left as is.
// (?!lazy\b) keeps the keyword in lazy patterns like: let lazy v = ... rendered as keyword
pattern: /(\b(?:and|let)\s+(?:rec\s+)?)(?!lazy\b)[a-z_][\w']*(?=\s+[a-z_~?(])/,
lookbehind: true
}
],
'class-name': [
{
// module name in a module path, e.g. My_module in My_module.my_function
pattern: /\b[A-Z][\w']*(?=\s*\.)/
},
{
// module name after module related keywords, e.g. open My_module
pattern: /(\b(?:include|module|open)\s+(?:type\s+)?(?:rec\s+)?)[A-Z][\w']*/,
lookbehind: true
}
]
})
Loading