From e2502ef52c8fbacd8d338871a6a5f14610a27ae8 Mon Sep 17 00:00:00 2001
From: Dan <39170265+chillenberger@users.noreply.github.com>
Date: Wed, 3 Jan 2024 15:17:31 -0700
Subject: [PATCH 1/3] use code mirror for syntax highlighting, fix code block
spacing
---
pgml-dashboard/package-lock.json | 234 ++++++++++++++-
pgml-dashboard/package.json | 8 +
pgml-dashboard/src/api/cms.rs | 91 ++++--
.../code_block/code_block_controller.js | 128 ++++++++
.../src/components/code_block/mod.rs | 14 +
.../src/components/code_block/template.html | 0
pgml-dashboard/src/components/mod.rs | 4 +
pgml-dashboard/src/utils/markdown.rs | 273 +++---------------
.../css/scss/components/_admonitions.scss | 3 +
.../static/css/scss/components/_code.scss | 4 +-
.../static/css/scss/pages/_docs.scss | 28 ++
pgml-dashboard/static/js/copy.js | 9 +
.../static/js/utilities/code_mirror_theme.js | 143 +++++++++
13 files changed, 675 insertions(+), 264 deletions(-)
create mode 100644 pgml-dashboard/src/components/code_block/code_block_controller.js
create mode 100644 pgml-dashboard/src/components/code_block/mod.rs
create mode 100644 pgml-dashboard/src/components/code_block/template.html
create mode 100644 pgml-dashboard/static/js/utilities/code_mirror_theme.js
diff --git a/pgml-dashboard/package-lock.json b/pgml-dashboard/package-lock.json
index 25740517e..40c5a3e84 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.11",
+ "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.11.tgz",
+ "integrity": "sha512-B5Y9EJF4BWiMgj4ufxUo2hrORnmMBDrMtR+L7dwIO5pocuSAahG6QBwXR6PbKJOjRywJczU2r2LJPg79ER91TQ==",
+ "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=="
},
"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..bdecd4996 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,69 @@ 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..0ecfda207
--- /dev/null
+++ b/pgml-dashboard/src/components/code_block/code_block_controller.js
@@ -0,0 +1,128 @@
+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 buildCustomTheme 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
+ buildCustomTheme(),
+ 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..53f6cb332 100644
--- a/pgml-dashboard/src/utils/markdown.rs
+++ b/pgml-dashboard/src/utils/markdown.rs
@@ -183,7 +183,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 +194,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"
+ "rust"
+ } else if options.starts_with("json") {
+ "json"
} else {
"code"
};
@@ -212,7 +216,7 @@ impl<'a> From<&str> for CodeFence<'a> {
CodeFence {
lang,
highlight,
- enumerate: options.contains("enumerate"),
+ line_numbers: options.contains("lineNumbers"),
}
}
}
@@ -220,233 +224,19 @@ impl<'a> From<&str> for CodeFence<'a> {
pub struct SyntaxHighlighter {}
impl SyntaxHighlighterAdapter for SyntaxHighlighter {
+
fn highlight(&self, options: Option<&str>, code: &str) -> String {
let code = if let Some(options) = options {
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 +251,7 @@ impl SyntaxHighlighterAdapter for SyntaxHighlighter {
code.to_string()
};
- format!(
- "",
- code
- )
+ code
}
fn build_pre_tag(&self, _attributes: &HashMap) -> String {
@@ -475,8 +262,16 @@ 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 +642,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,18 +670,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 {
@@ -940,7 +746,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
"#,
- if line_numbers {"line-numbers" } else {""},
+ if line_numbers { "line-numbers" } else { "" },
title
)),
None => Some(format!(
r#"
"#,
- if line_numbers {"line-numbers" } else {""},
- )
- ),
+ if line_numbers { "line-numbers" } else { "" },
+ )),
},
"results" => match &self.title {
Some(title) => Some(format!(
@@ -1020,7 +1024,11 @@ 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 line_numbers = parser(text.as_ref(), r#"lineNumbers=""#);
- let code_block = CodeBlock { time, title, line_numbers };
+ 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(
@@ -1036,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, line_numbers: None };
+ 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(