diff --git a/pgml-dashboard/package-lock.json b/pgml-dashboard/package-lock.json index 25740517e..c7f315dec 100644 --- a/pgml-dashboard/package-lock.json +++ b/pgml-dashboard/package-lock.json @@ -5,31 +5,259 @@ "packages": { "": { "dependencies": { + "@codemirror/lang-javascript": "^6.2.1", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-python": "^6.1.3", + "@codemirror/lang-rust": "^6.0.1", + "@codemirror/lang-sql": "^6.5.4", + "@codemirror/state": "^6.2.1", + "@codemirror/view": "^6.21.0", "autosize": "^6.0.1", + "codemirror": "^6.0.1", "dompurify": "^3.0.6", "marked": "^9.1.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.11.1.tgz", + "integrity": "sha512-L5UInv8Ffd6BPw0P3EF7JLYAMeEbclY7+6Q11REt8vhih8RuLreKtPy/xk8wPxs4EQgYqzI7cdgpiYwWlbS/ow==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.3.tgz", + "integrity": "sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.1.tgz", + "integrity": "sha512-jlFOXTejVyiQCW3EQwvKH0m99bUYIw40oPmFjSX2VS78yzfe0HELZ+NEo9Yfo1MkGRpGlj3Gnu4rdxV1EnAs5A==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.3.tgz", + "integrity": "sha512-S9w2Jl74hFlD5nqtUMIaXAq9t5WlM0acCkyuQWUUSvZclk1sV+UfnpFiZzuZSG+hfEaOmxKR5UxY/Uxswn7EhQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz", + "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.5.5.tgz", + "integrity": "sha512-DvOaP2RXLb2xlxJxxydTFfwyYw5YDqEFea6aAfgh9UH0kUD6J1KFZ0xPgPpw1eo/5s2w3L6uh5PVR7GM23GxkQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.0.tgz", + "integrity": "sha512-2vaNn9aPGCRFKWcHPFksctzJ8yS5p7YoaT+jHpc0UGKzNuAIx4qy6R5wiqbP+heEEdyaABA582mNqSHzSoYdmg==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.4.2.tgz", + "integrity": "sha512-wzRkluWb1ptPKdzlsrbwwjYCPLgzU6N88YBAmlZi8WFyuiEduSd05MnJYNogzyc8rPK7pj6m95ptUApc8sHKVA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.5.tgz", + "integrity": "sha512-PIEN3Ke1buPod2EHbJsoQwlbpkz30qGZKcnmH1eihq9+bPQx8gelauUwLYaY4vBOuBAuEhmpDLii4rj/uO0yMA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.0.tgz", + "integrity": "sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==" + }, + "node_modules/@codemirror/view": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.23.0.tgz", + "integrity": "sha512-/51px9N4uW8NpuWkyUX+iam5+PM6io2fm+QmRnzwqBy5v/pwGg9T0kILFtYeum8hjuvENtgsGNKluOfqIICmeQ==", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.0.tgz", + "integrity": "sha512-Wmvlm4q6tRpwiy20TnB3yyLTZim38Tkc50dPY8biQRwqE+ati/wD84rm3N15hikvdT4uSg9phs9ubjvcLmkpKg==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", + "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.12.tgz", + "integrity": "sha512-kwO5MftUiyfKBcECMEDc4HYnc10JME9kTJNPVoCXqJj/Y+ASWF0rgstORi3BThlQI6SoPSshrK5TjuiLFnr29A==", + "dependencies": { + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", + "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.14.tgz", + "integrity": "sha512-z5mY4LStlA3yL7aHT/rqgG614cfcvklS+8oFRFBYrs4YaWLJyKKM4+nN6KopToX0o9Hj6zmH6M5kinOYuy06ug==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.10.tgz", + "integrity": "sha512-pvSjn+OWivmA/si/SFeGouHO50xoOZcPIFzf8dql0gRvcfCvLDpVIpnnGFFlB7wa0WDscDLo0NmH+4Tx80nBdQ==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", + "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/autosize": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/autosize/-/autosize-6.0.1.tgz", "integrity": "sha512-f86EjiUKE6Xvczc4ioP1JBlWG7FKrE13qe/DxBCpe8GCipCq2nFw73aO8QEBKHfSbYGDN5eB9jXWKen7tspDqQ==" }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/dompurify": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.6.tgz", - "integrity": "sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==" + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.7.tgz", + "integrity": "sha512-BViYTZoqP3ak/ULKOc101y+CtHDUvBsVgSxIF1ku0HmK6BRf+C03MC+tArMvOPtVtZp83DDh5puywKDu4sbVjQ==" }, "node_modules/marked": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.0.tgz", - "integrity": "sha512-VZjm0PM5DMv7WodqOUps3g6Q7dmxs9YGiFUZ7a2majzQTTCgX+6S6NAJHPvOhgFBzYz8s4QZKWWMfZKFmsfOgA==", + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", "bin": { "marked": "bin/marked.js" }, "engines": { "node": ">= 16" } + }, + "node_modules/style-mod": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.0.tgz", + "integrity": "sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" } } } diff --git a/pgml-dashboard/package.json b/pgml-dashboard/package.json index 4347d2563..3dfc7d703 100644 --- a/pgml-dashboard/package.json +++ b/pgml-dashboard/package.json @@ -1,5 +1,13 @@ { "dependencies": { + "@codemirror/lang-javascript": "^6.2.1", + "@codemirror/lang-python": "^6.1.3", + "@codemirror/lang-rust": "^6.0.1", + "@codemirror/lang-sql": "^6.5.4", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/state": "^6.2.1", + "@codemirror/view": "^6.21.0", + "codemirror": "^6.0.1", "autosize": "^6.0.1", "dompurify": "^3.0.6", "marked": "^9.1.0" diff --git a/pgml-dashboard/src/api/cms.rs b/pgml-dashboard/src/api/cms.rs index 91eda8e0b..1f606b9f9 100644 --- a/pgml-dashboard/src/api/cms.rs +++ b/pgml-dashboard/src/api/cms.rs @@ -406,32 +406,6 @@ mod test { use rocket::local::asynchronous::Client; use rocket::{Build, Rocket}; - #[test] - fn test_syntax_highlighting() { - let code = r#" -# Hello - -```postgresql -SELECT * FROM test; -``` - "#; - - let arena = Arena::new(); - let root = parse_document(&arena, code, &options()); - - // Style headings like we like them - let mut plugins = ComrakPlugins::default(); - let binding = MarkdownHeadings::new(); - plugins.render.heading_adapter = Some(&binding); - plugins.render.codefence_syntax_highlighter = Some(&SyntaxHighlighter {}); - - let mut html = vec![]; - format_html_with_plugins(root, &options(), &mut html, &plugins).unwrap(); - let html = String::from_utf8(html).unwrap(); - - assert!(html.contains("SELECT")); - } - #[test] fn test_wrapping_tables() { let markdown = r#" @@ -574,4 +548,77 @@ This is the end of the markdown rsp.status() ); } + + // Test backend for line highlights and line numbers added + #[test] + fn gitbook_codeblock_test() { + let contents = r#" +{% code title="Test name for html" lineNumbers="true" %} +```javascript-highlightGreen="1" + import something + let a = 1 +``` +{% endcode %} +"#; + + let expected = r#" +
+
+ Test name for html +
+
+        
+ content_copy + link + edit +
+ +
importsomething
+
leta=1
+
+
+
+
"#; + + // Parse Markdown + let arena = Arena::new(); + let spaced_contents = crate::utils::markdown::gitbook_preprocess(contents); + let root = parse_document(&arena, &spaced_contents, &crate::utils::markdown::options()); + + crate::utils::markdown::wrap_tables(root, &arena).unwrap(); + + // MkDocs, gitbook syntax support, e.g. tabs, notes, alerts, etc. + crate::utils::markdown::mkdocs(root, &arena).unwrap(); + + // Style headings like we like them + let mut plugins = ComrakPlugins::default(); + let headings = crate::utils::markdown::MarkdownHeadings::new(); + plugins.render.heading_adapter = Some(&headings); + plugins.render.codefence_syntax_highlighter = + Some(&crate::utils::markdown::SyntaxHighlighter {}); + + let mut html = vec![]; + format_html_with_plugins( + root, + &crate::utils::markdown::options(), + &mut html, + &plugins, + ) + .unwrap(); + let html = String::from_utf8(html).unwrap(); + + println!("expected: {}", expected); + + println!("response: {}", html); + + assert!( + html.chars() + .filter(|c| !c.is_whitespace()) + .collect::() + == expected + .chars() + .filter(|c| !c.is_whitespace()) + .collect::() + ) + } } diff --git a/pgml-dashboard/src/components/code_block/code_block_controller.js b/pgml-dashboard/src/components/code_block/code_block_controller.js new file mode 100644 index 000000000..3a4f92483 --- /dev/null +++ b/pgml-dashboard/src/components/code_block/code_block_controller.js @@ -0,0 +1,130 @@ +import { Controller } from "@hotwired/stimulus"; +import { basicSetup } from "codemirror"; +import { sql } from "@codemirror/lang-sql"; +import { python } from "@codemirror/lang-python"; +import { javascript } from "@codemirror/lang-javascript"; +import { rust } from "@codemirror/lang-rust"; +import { json } from "@codemirror/lang-json"; +import { EditorView, ViewPlugin, Decoration } from "@codemirror/view"; +import { RangeSetBuilder, Facet} from "@codemirror/state"; +import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; + +import { highlightStyle, editorTheme } from "../../../static/js/utilities/code_mirror_theme"; + +const buildEditorView = (target, content, languageExtension, classes) => { + let editorView = new EditorView({ + doc: content, + extensions: [ + basicSetup, + languageExtension !== null ? languageExtension() : [], // if no language chosen do not highlight syntax + EditorView.theme(editorTheme), + syntaxHighlighting(HighlightStyle.define(highlightStyle)), + EditorView.contentAttributes.of({ contenteditable: false }), + addClasses.of(classes), + highlight + ], + parent: target, + highlightActiveLine: false + }); + return editorView; +}; + +const highlight = ViewPlugin.fromClass(class { + constructor(view) { + this.decorations = highlightLine(view) + } + + update(update) { + if (update.docChanged || update.viewportChanged) + this.decorations = highlightLine(update.view) + } +}, { + decorations: v => v.decorations +}) + +function highlightLine(view) { + let builder = new RangeSetBuilder() + let classes = view.state.facet(addClasses).shift() + for (let {from, to} of view.visibleRanges) { + for (let pos = from; pos <= to;) { + let lineClasses = classes.shift() + let line = view.state.doc.lineAt(pos) + builder.add(line.from, line.from, Decoration.line({attributes: {class: lineClasses}})) + pos = line.to + 1 + } + } + return builder.finish() +} + +const addClasses = Facet.define({ + combone: values => values +}) + +const language = (element) => { + switch (element.getAttribute("language")) { + case "sql": + return sql; + case "postgresql": + return sql; + case "python": + return python; + case "javascript": + return javascript; + case "rust": + return rust; + case "json": + return json; + default: + return null; + } +} + +const codeBlockCallback = (element) => { + let highlights = element.getElementsByClassName("highlight") + let classes = []; + for(let lineNum = 0; lineNum < highlights.length; lineNum++) { + classes.push(highlights[lineNum].classList) + } + + let content = element.textContent.trim() + element.innerHTML = ""; + + return [element, content, classes] +} + +// Add Codemirror with data controller +export default class extends Controller { + connect() { + let [element, content, classes] = codeBlockCallback(this.element) + let lang = language(this.element) + + buildEditorView(element, content, lang, classes); + } +} + +// Add Codemirror with web component +class CodeBlockA extends HTMLElement { + constructor() { + super(); + + this.language = language(this) + } + + connectedCallback() { + let [element, content, classes] = codeBlockCallback(this) + + buildEditorView(element, content, this.language, classes); + } + + // component attributes + static get observedAttributes() { + return ["type"]; + } + + // attribute change + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) return; + this[property] = newValue; + } +} +customElements.define("code-block", CodeBlockA); diff --git a/pgml-dashboard/src/components/code_block/mod.rs b/pgml-dashboard/src/components/code_block/mod.rs new file mode 100644 index 000000000..4a68d0a7b --- /dev/null +++ b/pgml-dashboard/src/components/code_block/mod.rs @@ -0,0 +1,14 @@ +use pgml_components::component; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce, Default)] +#[template(path = "code_block/template.html")] +pub struct CodeBlock {} + +impl CodeBlock { + pub fn new() -> CodeBlock { + CodeBlock {} + } +} + +component!(CodeBlock); diff --git a/pgml-dashboard/src/components/code_block/template.html b/pgml-dashboard/src/components/code_block/template.html new file mode 100644 index 000000000..e69de29bb diff --git a/pgml-dashboard/src/components/mod.rs b/pgml-dashboard/src/components/mod.rs index 373dbe776..d04961b77 100644 --- a/pgml-dashboard/src/components/mod.rs +++ b/pgml-dashboard/src/components/mod.rs @@ -16,6 +16,10 @@ pub use chatbot::Chatbot; // src/components/cms pub mod cms; +// src/components/code_block +pub mod code_block; +pub use code_block::CodeBlock; + // src/components/confirm_modal pub mod confirm_modal; pub use confirm_modal::ConfirmModal; diff --git a/pgml-dashboard/src/utils/markdown.rs b/pgml-dashboard/src/utils/markdown.rs index 999e8222e..fb0557aa2 100644 --- a/pgml-dashboard/src/utils/markdown.rs +++ b/pgml-dashboard/src/utils/markdown.rs @@ -8,7 +8,6 @@ use std::sync::{ Arc, }; -use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind}; use anyhow::Result; use comrak::{ adapters::{HeadingAdapter, HeadingMeta, SyntaxHighlighterAdapter}, @@ -17,7 +16,6 @@ use comrak::{ parse_document, Arena, ComrakExtensionOptions, ComrakOptions, ComrakRenderOptions, }; use itertools::Itertools; -use lazy_static::lazy_static; use regex::Regex; use tantivy::collector::TopDocs; use tantivy::query::{QueryParser, RegexQuery}; @@ -183,7 +181,7 @@ impl HighlightLines { struct CodeFence<'a> { lang: &'a str, highlight: HashMap, - enumerate: bool, + line_numbers: bool, } impl<'a> From<&str> for CodeFence<'a> { @@ -194,12 +192,16 @@ impl<'a> From<&str> for CodeFence<'a> { "bash" } else if options.starts_with("python") { "python" + } else if options.starts_with("javascript") { + "javascript" } else if options.starts_with("postgresql") { "postgresql" } else if options.starts_with("postgresql-line-nums") { "postgresql-line-nums" } else if options.starts_with("rust") { "rust" + } else if options.starts_with("json") { + "json" } else { "code" }; @@ -212,7 +214,7 @@ impl<'a> From<&str> for CodeFence<'a> { CodeFence { lang, highlight, - enumerate: options.contains("enumerate"), + line_numbers: options.contains("lineNumbers"), } } } @@ -225,228 +227,13 @@ impl SyntaxHighlighterAdapter for SyntaxHighlighter { let code = code.to_string(); let options = CodeFence::from(options); - let code = match options.lang { - "postgresql" | "sql" | "postgresql-line-nums" => { - lazy_static! { - static ref SQL_KEYS: [&'static str; 69] = [ - "PARTITION OF", - "PARTITION BY", - "CASCADE", - "INNER ", - "ON ", - "WITH", - "SELECT", - "UPDATE", - "DELETE", - "WHERE", - "AS", - "HAVING", - "ORDER BY", - "ASC", - "DESC", - "LIMIT", - "FROM", - "CREATE", - "REPLACE", - "DROP", - "VIEW", - "EXTENSION", - "SERVER", - "FOREIGN DATA WRAPPER", - "OPTIONS", - "IMPORT FOREIGN SCHEMA", - "CREATE USER MAPPING", - "INTO", - "PUBLICATION", - "FOR", - "ALL", - "TABLES", - "CONNECTION", - "SUBSCRIPTION", - "JOIN", - "INTO", - "INSERT", - "BEGIN", - "ALTER", - "SCHEMA", - "RENAME", - "COMMIT", - "AND ", - "ADD COLUMN", - "ALTER TABLE", - "PRIMARY KEY", - "DO", - "END", - "BETWEEN", - "SET", - "REINDEX", - "INDEX", - "USING", - "GROUP BY", - "CREATE TABLE", - "pgml.embed", - "pgml.sum", - "pgml.norm_l2", - "CONCURRENTLY", - "ON\n", - "VALUES", - "@@", - "=>", - "GENERATED ALWAYS AS", - "STORED", - "IF NOT EXISTS", - "pgml.train", - "pgml.predict", - "pgml.transform", - ]; - static ref SQL_KEYS_REPLACEMENTS: [&'static str; 69] = [ - r#"PARTITION OF"#, - r#"PARTITION BY"#, - "CASCADE", - "INNER ", - "ON ", - "WITH", - "SELECT", - "UPDATE", - "DELETE", - "WHERE", - "AS", - "HAVING", - "ORDER BY", - "ASC", - "DESC", - "LIMIT", - "FROM", - "CREATE", - "REPLACE", - "DROP", - "VIEW", - "EXTENSION", - "SERVER", - "FOREIGN DATA WRAPPER", - "OPTIONS", - "IMPORT FOREIGN SCHEMA", - "CREATE USER MAPPING", - "INTO", - "PUBLICATION", - "FOR", - "ALL", - "TABLES", - "CONNECTION", - "SUBSCRIPTION", - "JOIN", - "INTO", - "INSERT", - "BEGIN", - "ALTER", - "SCHEMA", - "RENAME", - "COMMIT", - "AND ", - "ADD COLUMN", - "ALTER TABLE", - "PRIMARY KEY", - "DO", - "END", - "BETWEEN", - "SET", - "REINDEX", - "INDEX", - "USING", - "GROUP BY", - "CREATE TABLE", - "pgml.embed", - "pgml.sum", - "pgml.norm_l2", - "CONCURRENTLY", - "ON\n", - "VALUES", - "@@", - "=>", - "GENERATED ALWAYS AS", - "STORED", - "IF NOT EXISTS", - "pgml.train", - "pgml.predict", - "pgml.transform", - ]; - static ref AHO_SQL: AhoCorasick = AhoCorasickBuilder::new() - .match_kind(MatchKind::LeftmostLongest) - .build(SQL_KEYS.iter()); - } - - AHO_SQL - .replace_all(&code, &SQL_KEYS_REPLACEMENTS[..]) - .to_string() - } - - "bash" => { - lazy_static! { - static ref RE_BASH: regex::Regex = regex::Regex::new(r"(cd)").unwrap(); - } - - RE_BASH - .replace_all(&code, r#"$1"#) - .to_string() - } - - "python" => { - lazy_static! { - static ref RE_PYTHON: regex::Regex = regex::Regex::new( - r"(import |def |return |if |else|class |async |await )" - ) - .unwrap(); - } - - RE_PYTHON - .replace_all(&code, r#"$1"#) - .to_string() - } - - "rust" => { - lazy_static! { - static ref RE_RUST: regex::Regex = regex::Regex::new( - r"(struct |let |pub |fn |await |impl |const |use |type |move |if |else| |match |for |enum)" - ) - .unwrap(); - } - - RE_RUST - .replace_all(&code, r#"$1"#) - .to_string() - } - - _ => code, - }; - - // Add line numbers - let code = if options.enumerate { - let mut code = code.split('\n') - .enumerate() - .map(|(index, code)| { - format!(r#"{}{}"#, - if index < 9 {format!(" {}", index+1)} else { format!("{}", index+1)}, - code) - }) - .collect::>(); - code.pop(); - code.into_iter().join("\n") - } else { - let mut code = code - .split('\n') - .map(|code| format!("{}", code)) - .collect::>(); - code.pop(); - code.into_iter().join("\n") - }; - // Add line highlighting let code = code .split('\n') .enumerate() .map(|(index, code)| { format!( - r#"
{}
"#, + r#"
{}
"#, match options.highlight.get(&(index + 1).to_string()) { Some(color) => color, _ => "none", @@ -461,10 +248,7 @@ impl SyntaxHighlighterAdapter for SyntaxHighlighter { code.to_string() }; - format!( - "
{}
", - code - ) + code } fn build_pre_tag(&self, _attributes: &HashMap) -> String { @@ -475,8 +259,24 @@ impl SyntaxHighlighterAdapter for SyntaxHighlighter { ") } - fn build_code_tag(&self, _attributes: &HashMap) -> String { - String::from("") + fn build_code_tag(&self, attributes: &HashMap) -> String { + let data = match attributes.get("class") { + Some(lang) => lang.replace("language-", ""), + _ => "".to_string(), + }; + + let parsed_data = CodeFence::from(data.as_str()); + + // code-block web component uses codemirror to add syntax highlighting + format!( + "", + if parsed_data.line_numbers { + "class='line-numbers'" + } else { + "" + }, + parsed_data.lang, + ) } } @@ -847,10 +647,19 @@ impl From<&str> for Admonition { struct CodeBlock { time: Option, title: Option, + line_numbers: Option, } impl CodeBlock { fn html(&self, html_type: &str) -> Option { + let line_numbers: bool = match &self.line_numbers { + Some(val) => match val.as_str() { + "true" => true, + _ => false, + }, + _ => false, + }; + match html_type { "time" => self.time.as_ref().map(|time| { format!( @@ -866,19 +675,20 @@ impl CodeBlock { "code" => match &self.title { Some(title) => Some(format!( r#" -
+
{}
"#, + if line_numbers { "line-numbers" } else { "" }, title )), - None => Some( + None => Some(format!( r#" -
- "# - .to_string(), - ), +
+ "#, + if line_numbers { "line-numbers" } else { "" }, + )), }, "results" => match &self.title { Some(title) => Some(format!( @@ -940,7 +750,7 @@ pub fn gitbook_preprocess(item: &str) -> String { pub fn mkdocs<'a>(root: &'a AstNode<'a>, arena: &'a Arena>) -> anyhow::Result<()> { let mut tabs = Vec::new(); - // tracks open !!! blocks and holds items to apppend prior to closing + // tracks openning tags and holds items to apppend prior to closing let mut info_block_close_items: Vec> = vec![]; iter_nodes(root, &mut |node| { @@ -1213,7 +1023,12 @@ pub fn mkdocs<'a>(root: &'a AstNode<'a>, arena: &'a Arena>) -> anyho let title = parser(text.as_ref(), r#"title=""#); let time = parser(text.as_ref(), r#"time=""#); - let code_block = CodeBlock { time, title }; + let line_numbers = parser(text.as_ref(), r#"lineNumbers=""#); + let code_block = CodeBlock { + time, + title, + line_numbers, + }; if let Some(html) = code_block.html("code") { let n = arena.alloc(Node::new(RefCell::new(Ast::new( @@ -1229,7 +1044,11 @@ pub fn mkdocs<'a>(root: &'a AstNode<'a>, arena: &'a Arena>) -> anyho let parent = node.parent().unwrap(); let title = parser(text.as_ref(), r#"title=""#); - let code_block = CodeBlock { time: None, title }; + let code_block = CodeBlock { + time: None, + title, + line_numbers: None, + }; if let Some(html) = code_block.html("results") { let n = arena.alloc(Node::new(RefCell::new(Ast::new( diff --git a/pgml-dashboard/static/css/scss/components/_admonitions.scss b/pgml-dashboard/static/css/scss/components/_admonitions.scss index e145e7dc8..6e3dde527 100644 --- a/pgml-dashboard/static/css/scss/components/_admonitions.scss +++ b/pgml-dashboard/static/css/scss/components/_admonitions.scss @@ -69,6 +69,9 @@ pre { margin: 0px; } + pre[data-controller="copy"] { + padding-top: 2rem !important; + } div.code-block { border: none !important; diff --git a/pgml-dashboard/static/css/scss/components/_code.scss b/pgml-dashboard/static/css/scss/components/_code.scss index 0545363cd..f7c97f2a0 100644 --- a/pgml-dashboard/static/css/scss/components/_code.scss +++ b/pgml-dashboard/static/css/scss/components/_code.scss @@ -64,7 +64,7 @@ pre { display: inline-block; width: 100%; @if $color { - background-color: $color; + background-color: $color !important; } } @@ -110,7 +110,7 @@ pre { } .code-line-highlight-none { @include code-line-highlight(null); - } + } .code-line-numbers { @extend .noselect; diff --git a/pgml-dashboard/static/css/scss/pages/_docs.scss b/pgml-dashboard/static/css/scss/pages/_docs.scss index ad71f1564..4fca4c7ae 100644 --- a/pgml-dashboard/static/css/scss/pages/_docs.scss +++ b/pgml-dashboard/static/css/scss/pages/_docs.scss @@ -178,5 +178,33 @@ margin-right: auto; } } + + // Codemirror overrideds + .cm-editor { + background: inherit; + + // default no line numbers. + .cm-gutters { + display: none; + } + } + + .cm-gutters { + background: inherit; + } + + .code-highlight { + background: blue; + } + + .cm-activeLine { + background-color: transparent; + } + + .line-numbers { + .cm-gutters { + display: contents !important; + } + } } diff --git a/pgml-dashboard/static/js/copy.js b/pgml-dashboard/static/js/copy.js index a7b45eda5..a5c9ba343 100644 --- a/pgml-dashboard/static/js/copy.js +++ b/pgml-dashboard/static/js/copy.js @@ -9,10 +9,19 @@ import { export default class extends Controller { codeCopy() { + + // mkdocs / original style code let text = [...this.element.querySelectorAll('span.code-content')] .map((copied) => copied.innerText) .join('\n') + // codemirror style code + if (text.length === 0 ) { + text = [...this.element.querySelectorAll('div.cm-line')] + .map((copied) => copied.innerText) + .join('\n') + } + if (text.length === 0) { text = this.element.innerText.replace('content_copy', '') } diff --git a/pgml-dashboard/static/js/utilities/code_mirror_theme.js b/pgml-dashboard/static/js/utilities/code_mirror_theme.js new file mode 100644 index 000000000..4ec9ca39e --- /dev/null +++ b/pgml-dashboard/static/js/utilities/code_mirror_theme.js @@ -0,0 +1,139 @@ +import { tags as t } from "@lezer/highlight"; + +// Theme builder is taken from: https://github.com/codemirror/theme-one-dark#readme + +const chalky = "#FF0"; // Set +const coral = "#F5708B"; // Set +const salmon = "#e9467a"; +const blue = "#00e0ff"; +const cyan = "#56b6c2"; +const invalid = "#ffffff"; +const ivory = "#abb2bf"; +const stone = "#7d8799"; +const malibu = "#61afef"; +const sage = "#0F0"; // Set +const whiskey = "#d19a66"; +const violet = "#F3F"; // Set +const darkBackground = "#17181A"; // Set +const highlightBackground = "#2c313a"; +const background = "#17181A"; // Set +const tooltipBackground = "#353a42"; +const selection = "#3E4451"; +const cursor = "#528bff"; + +const editorTheme = { + "&": { + color: ivory, + backgroundColor: background, + }, + + ".cm-content": { + caretColor: cursor, + }, + + ".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor }, + "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": + { backgroundColor: selection }, + + ".cm-panels": { backgroundColor: darkBackground, color: ivory }, + ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, + ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, + + ".cm-searchMatch": { + backgroundColor: "#72a1ff59", + outline: "1px solid #457dff", + }, + ".cm-searchMatch.cm-searchMatch-selected": { + backgroundColor: "#6199ff2f", + }, + + ".cm-activeLine": { backgroundColor: "#6699ff0b" }, + ".cm-selectionMatch": { backgroundColor: "#aafe661a" }, + + "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { + backgroundColor: "#bad0f847", + }, + + ".cm-gutters": { + backgroundColor: background, + color: stone, + border: "none", + }, + + ".cm-activeLineGutter": { + backgroundColor: highlightBackground, + }, + + ".cm-foldPlaceholder": { + backgroundColor: "transparent", + border: "none", + color: "#ddd", + }, + + ".cm-tooltip": { + border: "none", + backgroundColor: tooltipBackground, + }, + ".cm-tooltip .cm-tooltip-arrow:before": { + borderTopColor: "transparent", + borderBottomColor: "transparent", + }, + ".cm-tooltip .cm-tooltip-arrow:after": { + borderTopColor: tooltipBackground, + borderBottomColor: tooltipBackground, + }, + ".cm-tooltip-autocomplete": { + "& > ul > li[aria-selected]": { + backgroundColor: highlightBackground, + color: ivory, + }, + }, +} + +const highlightStyle = [ + { tag: t.keyword, color: violet }, + { + tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], + color: salmon, + }, + { tag: [t.function(t.variableName), t.labelName], color: malibu }, + { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey }, + { tag: [t.definition(t.name), t.separator], color: ivory }, + { + tag: [ + t.typeName, + t.className, + t.number, + t.changed, + t.annotation, + t.modifier, + t.self, + t.namespace, + ], + color: chalky, + }, + { + tag: [ + t.operator, + t.operatorKeyword, + t.url, + t.escape, + t.regexp, + t.link, + t.special(t.string), + ], + color: blue, + }, + { tag: [t.meta, t.comment], color: stone }, + { tag: t.strong, fontWeight: "bold" }, + { tag: t.emphasis, fontStyle: "italic" }, + { tag: t.strikethrough, textDecoration: "line-through" }, + { tag: t.link, color: stone, textDecoration: "underline" }, + { tag: t.heading, fontWeight: "bold", color: salmon }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey }, + { tag: [t.processingInstruction, t.string, t.inserted], color: sage }, + { tag: t.invalid, color: invalid }, +] + + +export {highlightStyle, editorTheme}; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy