diff --git a/package-lock.json b/package-lock.json index 38884be..e091273 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2796,6 +2796,13 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.4", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.4.tgz", + "integrity": "sha512-rlAnzkW2sZOjbqZ743IHUhFcvzaGbqijwOu8QZnZCjfQzBqFE3s4lOTJEsxikImav9uzz/42I+O7YUs1mWgMlg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -4052,6 +4059,26 @@ "node": ">= 10" } }, + "node_modules/clipboard-copy": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clipboard-copy/-/clipboard-copy-4.0.1.tgz", + "integrity": "sha512-wOlqdqziE/NNTUJsfSgXmBMIrYmfd5V0HCGsR8uAKHcg+h9NENWINcfRjtWGU77wDHC8B8ijV4hMTGYbrKovng==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -8367,6 +8394,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -10849,8 +10885,10 @@ "@matejmazur/react-katex": "^3.1.3", "@types/he": "^1.2.3", "@types/katex": "^0.16.7", + "clipboard-copy": "^4.0.1", "he": "^1.2.0", "katex": "^0.16.11", + "prismjs": "^1.29.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet": "^6.1.0" @@ -10858,6 +10896,7 @@ "devDependencies": { "@cozy-blog/notion-client": "^0.0.22", "@eslint/js": "^9.9.0", + "@types/prismjs": "^1.26.4", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-helmet": "^6.1.11", diff --git a/packages/react-notion-custom/CONTRIBUTING-KR.md b/packages/react-notion-custom/CONTRIBUTING-KR.md index 7c1dd3c..78b6274 100644 --- a/packages/react-notion-custom/CONTRIBUTING-KR.md +++ b/packages/react-notion-custom/CONTRIBUTING-KR.md @@ -654,7 +654,7 @@ fetchNotionPage(); | Quote | ✅ Yes | `quote` | | | Callout | ✅ Yes | `callout` | | | Equation | ❌ No | `equation` | | -| Code | ❌ No | `code` | | +| Code | ✅ Yes | `code` | | | Image | ❌ No | `image` | | | Video | ✅ Yes | `video` | | | Bookmark | ❌ No | `bookmark` | | diff --git a/packages/react-notion-custom/CONTRIBUTING.md b/packages/react-notion-custom/CONTRIBUTING.md index 702b799..2ee3fd6 100644 --- a/packages/react-notion-custom/CONTRIBUTING.md +++ b/packages/react-notion-custom/CONTRIBUTING.md @@ -657,7 +657,7 @@ Here's a list of Notion block types currently supported in react-notion-custom. | Quote | ✅ Yes | `quote` | | | Callout | ✅ Yes | `callout` | | | Equation | ❌ No | `equation` | | -| Code | ❌ No | `code` | | +| Code | ✅ Yes | `code` | | | Image | ❌ No | `image` | | | Video | ✅ Yes | `video` | | | Bookmark | ❌ No | `bookmark` | | diff --git a/packages/react-notion-custom/package.json b/packages/react-notion-custom/package.json index 617b5bd..5800a47 100644 --- a/packages/react-notion-custom/package.json +++ b/packages/react-notion-custom/package.json @@ -13,8 +13,10 @@ "@matejmazur/react-katex": "^3.1.3", "@types/he": "^1.2.3", "@types/katex": "^0.16.7", + "clipboard-copy": "^4.0.1", "he": "^1.2.0", "katex": "^0.16.11", + "prismjs": "^1.29.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet": "^6.1.0" @@ -22,6 +24,7 @@ "devDependencies": { "@cozy-blog/notion-client": "^0.0.22", "@eslint/js": "^9.9.0", + "@types/prismjs": "^1.26.4", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-helmet": "^6.1.11", diff --git a/packages/react-notion-custom/src/lib/components/code/index.tsx b/packages/react-notion-custom/src/lib/components/code/index.tsx new file mode 100644 index 0000000..ac4651c --- /dev/null +++ b/packages/react-notion-custom/src/lib/components/code/index.tsx @@ -0,0 +1,99 @@ +import { useState, useEffect } from "react"; +import copyToClipboard from "clipboard-copy"; +import RichText from "../internal/rich-text"; +import Prisim from "prismjs"; +import "prismjs/themes/prism.css"; +import loadDart from "./support-language/dart"; +import loadElixir from "./support-language/elixir"; +import loadGo from "./support-language/go"; +import loadJava from "./support-language/java"; +import loadKotlin from "./support-language/kotlin"; +import loadMarkdown from "./support-language/markdown"; +import loadPython from "./support-language/python"; +import loadSql from "./support-language/sql"; +import loadTypescript from "./support-language/typescript"; +import type { CodeArgs } from "../../types"; + +[ + loadDart, + loadTypescript, + loadElixir, + loadGo, + loadJava, + loadKotlin, + loadMarkdown, + loadPython, + loadSql, +].forEach((load) => load(Prisim)); + +const Code = ({ ...props }: CodeArgs) => { + const { + code: { caption, rich_text: texts, language }, + } = props; + const [showTooltip, setShowTooltip] = useState(false); + const content = texts.map(({ plain_text }) => plain_text).join(""); + + useEffect(() => { + let timer: NodeJS.Timeout; + if (showTooltip) { + timer = setTimeout(() => { + setShowTooltip(false); + }, 2000); + } + return () => clearTimeout(timer); + }, [showTooltip]); + + const handleCopy = () => { + copyToClipboard(content); + setShowTooltip(true); + }; + + return ( +
+
+
+
+ {language.replace(/^[a-z]/, (char) => char.toUpperCase())} +
+ +
+ +
+ +
+ {caption.length !== 0 && ( +
+ +
+ )} +
+
{"Copied"}
+
+
+ ); +}; + +export default Code; diff --git a/packages/react-notion-custom/src/lib/components/code/support-language/dart.ts b/packages/react-notion-custom/src/lib/components/code/support-language/dart.ts new file mode 100644 index 0000000..5cd07b8 --- /dev/null +++ b/packages/react-notion-custom/src/lib/components/code/support-language/dart.ts @@ -0,0 +1,92 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +let loaded = false; + +export default function load(Prism: any) { + if (loaded) return; + _load(Prism); + loaded = true; +} + +function _load(Prism: any) { + const keywords = [ + /\b(?:async|sync|yield)\*/, + /\b(?:abstract|assert|async|await|break|case|catch|class|const|continue|covariant|default|deferred|do|dynamic|else|enum|export|extends|extension|external|factory|final|finally|for|get|hide|if|implements|import|in|interface|library|mixin|new|null|on|operator|part|rethrow|return|set|show|static|super|switch|sync|this|throw|try|typedef|var|void|while|with|yield)\b/, + ]; + + // Handles named imports, such as http.Client + const packagePrefix = /(^|[^\w.])(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/ + .source; + + // based on the dart naming conventions + const className = { + pattern: RegExp(packagePrefix + /[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source), + lookbehind: true, + inside: { + namespace: { + pattern: /^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/, + inside: { + punctuation: /\./, + }, + }, + }, + }; + + Prism.languages.dart = Prism.languages.extend("clike", { + "class-name": [ + className, + { + // variables and parameters + // this to support class names (or generic parameters) which do not contain a lower case letter (also works for methods) + pattern: RegExp(packagePrefix + /[A-Z]\w*(?=\s+\w+\s*[;,=()])/.source), + lookbehind: true, + inside: className.inside, + }, + ], + keyword: keywords, + operator: + /\bis!|\b(?:as|is)\b|\+\+|--|&&|\|\||<<=?|>>=?|~(?:\/=?)?|[+\-*/%&^|=!<>]=?|\?/, + }); + + Prism.languages.insertBefore("dart", "string", { + "string-literal": { + pattern: + /r?(?:("""|''')[\s\S]*?\1|(["'])(?:\\.|(?!\2)[^\\\r\n])*\2(?!\2))/, + greedy: true, + inside: { + interpolation: { + pattern: /((?:^|[^\\])(?:\\{2})*)\$(?:\w+|\{(?:[^{}]|\{[^{}]*\})*\})/, + lookbehind: true, + inside: { + punctuation: /^\$\{?|\}$/, + expression: { + pattern: /[\s\S]+/, + inside: Prism.languages.dart, + }, + }, + }, + string: /[\s\S]+/, + }, + }, + string: undefined, + }); + + Prism.languages.insertBefore("dart", "class-name", { + metadata: { + pattern: /@\w+/, + alias: "function", + }, + }); + + Prism.languages.insertBefore("dart", "class-name", { + generics: { + pattern: + /<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<[\w\s,.&?]*>)*>)*>)*>/, + inside: { + "class-name": className, + keyword: keywords, + punctuation: /[<>(),.:]/, + operator: /[?&|]/, + }, + }, + }); +} diff --git a/packages/react-notion-custom/src/lib/components/code/support-language/elixir.ts b/packages/react-notion-custom/src/lib/components/code/support-language/elixir.ts new file mode 100644 index 0000000..a431f45 --- /dev/null +++ b/packages/react-notion-custom/src/lib/components/code/support-language/elixir.ts @@ -0,0 +1,114 @@ +/* eslint-disable no-useless-escape */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +let loaded = false; + +export default function load(Prism: any) { + if (loaded) return; + _load(Prism); + loaded = true; +} + +function _load(Prism: any) { + Prism.languages.elixir = { + doc: { + pattern: + /@(?:doc|moduledoc)\s+(?:("""|''')[\s\S]*?\1|("|')(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2)/, + inside: { + attribute: /^@\w+/, + string: /['"][\s\S]+/, + }, + }, + comment: { + pattern: /#.*/, + greedy: true, + }, + // ~r"""foo""" (multi-line), ~r'''foo''' (multi-line), ~r/foo/, ~r|foo|, ~r"foo", ~r'foo', ~r(foo), ~r[foo], ~r{foo}, ~r + regex: { + pattern: + /~[rR](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|[^\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[uismxfr]*/, + greedy: true, + }, + string: [ + { + // ~s"""foo""" (multi-line), ~s'''foo''' (multi-line), ~s/foo/, ~s|foo|, ~s"foo", ~s'foo', ~s(foo), ~s[foo], ~s{foo} (with interpolation care), ~s + pattern: + /~[cCsSwW](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|#\{[^}]+\}|#(?!\{)|[^#\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[csa]?/, + greedy: true, + inside: { + // See interpolation below + }, + }, + { + pattern: /("""|''')[\s\S]*?\1/, + greedy: true, + inside: { + // See interpolation below + }, + }, + { + // Multi-line strings are allowed + pattern: /("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, + greedy: true, + inside: { + // See interpolation below + }, + }, + ], + atom: { + // Look-behind prevents bad highlighting of the :: operator + pattern: /(^|[^:]):\w+/, + lookbehind: true, + alias: "symbol", + }, + module: { + pattern: /\b[A-Z]\w*\b/, + alias: "class-name", + }, + // Look-ahead prevents bad highlighting of the :: operator + "attr-name": /\b\w+\??:(?!:)/, + argument: { + // Look-behind prevents bad highlighting of the && operator + pattern: /(^|[^&])&\d+/, + lookbehind: true, + alias: "variable", + }, + attribute: { + pattern: /@\w+/, + alias: "variable", + }, + function: /\b[_a-zA-Z]\w*[?!]?(?:(?=\s*(?:\.\s*)?\()|(?=\/\d))/, + number: /\b(?:0[box][a-f\d_]+|\d[\d_]*)(?:\.[\d_]+)?(?:e[+-]?[\d_]+)?\b/i, + keyword: + /\b(?:after|alias|and|case|catch|cond|def(?:callback|delegate|exception|impl|macro|module|n|np|p|protocol|struct)?|do|else|end|fn|for|if|import|not|or|quote|raise|require|rescue|try|unless|unquote|use|when)\b/, + boolean: /\b(?:false|nil|true)\b/, + operator: [ + /\bin\b|&&?|\|[|>]?|\\\\|::|\.\.\.?|\+\+?|-[->]?|<[-=>]|>=|!==?|\B!|=(?:==?|[>~])?|[*\/^]/, + { + // We don't want to match << + pattern: /([^<])<(?!<)/, + lookbehind: true, + }, + { + // We don't want to match >> + pattern: /([^>])>(?!>)/, + lookbehind: true, + }, + ], + punctuation: /<<|>>|[.,%[\]{}()]/, + }; + + Prism.languages.elixir.string.forEach(function (o: any) { + o.inside = { + interpolation: { + pattern: /#\{[^}]+\}/, + inside: { + delimiter: { + pattern: /^#\{|\}$/, + alias: "punctuation", + }, + rest: Prism.languages.elixir, + }, + }, + }; + }); +} diff --git a/packages/react-notion-custom/src/lib/components/code/support-language/go.ts b/packages/react-notion-custom/src/lib/components/code/support-language/go.ts new file mode 100644 index 0000000..352803a --- /dev/null +++ b/packages/react-notion-custom/src/lib/components/code/support-language/go.ts @@ -0,0 +1,43 @@ +/* eslint-disable no-useless-escape */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +let loaded = false; + +export default function load(Prism: any) { + if (loaded) return; + _load(Prism); + loaded = true; +} + +function _load(Prism: any) { + Prism.languages.go = Prism.languages.extend("clike", { + string: { + pattern: /(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/, + lookbehind: true, + greedy: true, + }, + keyword: + /\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|const)\b/, + boolean: /\b(?:_|false|iota|nil|true)\b/, + number: [ + // binary and octal integers + /\b0(?:b[01_]+|o[0-7_]+)i?\b/i, + // hexadecimal integers and floats + /\b0x(?:[a-f\d_]+(?:\.[a-f\d_]*)?|\.[a-f\d_]+)(?:p[+-]?\d+(?:_\d+)*)?i?(?!\w)/i, + // decimal integers and floats + /(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?[\d_]+)?i?(?!\w)/i, + ], + operator: + /[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./, + builtin: + /\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\b/, + }); + + Prism.languages.insertBefore("go", "string", { + char: { + pattern: /'(?:\\.|[^'\\\r\n]){0,10}'/, + greedy: true, + }, + }); + + delete Prism.languages.go["class-name"]; +} diff --git a/packages/react-notion-custom/src/lib/components/code/support-language/java.ts b/packages/react-notion-custom/src/lib/components/code/support-language/java.ts new file mode 100644 index 0000000..8a47d05 --- /dev/null +++ b/packages/react-notion-custom/src/lib/components/code/support-language/java.ts @@ -0,0 +1,164 @@ +/* eslint-disable no-useless-escape */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +let loaded = false; + +export default function load(Prism: any) { + if (loaded) return; + _load(Prism); + loaded = true; +} + +function _load(Prism: any) { + const keywords = + /\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\s*[(){}[\]<>=%~.:,;?+\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|const|void|volatile|while|with|yield)\b/; + + // full package (optional) + parent classes (optional) + const classNamePrefix = /(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/.source; + + // based on the java naming conventions + const className = { + pattern: RegExp( + /(^|[^\w.])/.source + + classNamePrefix + + /[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source, + ), + lookbehind: true, + inside: { + namespace: { + pattern: /^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/, + inside: { + punctuation: /\./, + }, + }, + punctuation: /\./, + }, + }; + + Prism.languages.java = Prism.languages.extend("clike", { + string: { + pattern: /(^|[^\\])"(?:\\.|[^"\\\r\n])*"/, + lookbehind: true, + greedy: true, + }, + "class-name": [ + className, + { + // constiables, parameters, and constructor references + // this to support class names (or generic parameters) which do not contain a lower case letter (also works for methods) + pattern: RegExp( + /(^|[^\w.])/.source + + classNamePrefix + + /[A-Z]\w*(?=\s+\w+\s*[;,=()]|\s*(?:\[[\s,]*\]\s*)?::\s*new\b)/ + .source, + ), + lookbehind: true, + inside: className.inside, + }, + { + // class names based on keyword + // this to support class names (or generic parameters) which do not contain a lower case letter (also works for methods) + pattern: RegExp( + /(\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\s+)/ + .source + + classNamePrefix + + /[A-Z]\w*\b/.source, + ), + lookbehind: true, + inside: className.inside, + }, + ], + keyword: keywords, + function: [ + Prism.languages.clike.function, + { + pattern: /(::\s*)[a-z_]\w*/, + lookbehind: true, + }, + ], + number: + /\b0b[01][01_]*L?\b|\b0x(?:\.[\da-f_p+-]+|[\da-f_]+(?:\.[\da-f_p+-]+)?)\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i, + operator: { + pattern: + /(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m, + lookbehind: true, + }, + constant: /\b[A-Z][A-Z_\d]+\b/, + }); + + Prism.languages.insertBefore("java", "string", { + "triple-quoted-string": { + // http://openjdk.java.net/jeps/355#Description + pattern: /"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/, + greedy: true, + alias: "string", + }, + char: { + pattern: /'(?:\\.|[^'\\\r\n]){1,6}'/, + greedy: true, + }, + }); + + Prism.languages.insertBefore("java", "class-name", { + annotation: { + pattern: /(^|[^.])@\w+(?:\s*\.\s*\w+)*/, + lookbehind: true, + alias: "punctuation", + }, + generics: { + pattern: + /<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&))*>)*>)*>)*>/, + inside: { + "class-name": className, + keyword: keywords, + punctuation: /[<>(),.:]/, + operator: /[?&|]/, + }, + }, + import: [ + { + pattern: RegExp( + /(\bimport\s+)/.source + + classNamePrefix + + /(?:[A-Z]\w*|\*)(?=\s*;)/.source, + ), + lookbehind: true, + inside: { + namespace: className.inside.namespace, + punctuation: /\./, + operator: /\*/, + "class-name": /\w+/, + }, + }, + { + pattern: RegExp( + /(\bimport\s+static\s+)/.source + + classNamePrefix + + /(?:\w+|\*)(?=\s*;)/.source, + ), + lookbehind: true, + alias: "static", + inside: { + namespace: className.inside.namespace, + static: /\b\w+$/, + punctuation: /\./, + operator: /\*/, + "class-name": /\w+/, + }, + }, + ], + namespace: { + pattern: RegExp( + /(\b(?:exports|import(?:\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\s+)(?!)[a-z]\w*(?:\.[a-z]\w*)*\.?/.source.replace( + //g, + function () { + return keywords.source; + }, + ), + ), + lookbehind: true, + inside: { + punctuation: /\./, + }, + }, + }); +} diff --git a/packages/react-notion-custom/src/lib/components/code/support-language/kotlin.ts b/packages/react-notion-custom/src/lib/components/code/support-language/kotlin.ts new file mode 100644 index 0000000..fb29daf --- /dev/null +++ b/packages/react-notion-custom/src/lib/components/code/support-language/kotlin.ts @@ -0,0 +1,101 @@ +/* eslint-disable no-useless-escape */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +let loaded = false; + +export default function load(Prism: any) { + if (loaded) return; + _load(Prism); + loaded = true; +} + +function _load(Prism: any) { + Prism.languages.kotlin = Prism.languages.extend("clike", { + keyword: { + // The lookbehind prevents wrong highlighting of e.g. kotlin.properties.get + pattern: + /(^|[^.])\b(?:abstract|actual|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|dynamic|else|enum|expect|external|final|finally|for|fun|get|if|import|in|infix|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|operator|out|override|package|private|protected|public|reified|return|sealed|set|super|suspend|tailrec|this|throw|to|try|typealias|val|const|constarg|when|where|while)\b/, + lookbehind: true, + }, + function: [ + { + pattern: /(?:`[^\r\n`]+`|\b\w+)(?=\s*\()/, + greedy: true, + }, + { + pattern: /(\.)(?:`[^\r\n`]+`|\w+)(?=\s*\{)/, + lookbehind: true, + greedy: true, + }, + ], + number: + /\b(?:0[xX][\da-fA-F]+(?:_[\da-fA-F]+)*|0[bB][01]+(?:_[01]+)*|\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?(?:[eE][+-]?\d+(?:_\d+)*)?[fFL]?)\b/, + operator: + /\+[+=]?|-[-=>]?|==?=?|!(?:!|==?)?|[\/*%<>]=?|[?:]:?|\.\.|&&|\|\||\b(?:and|inv|or|shl|shr|ushr|xor)\b/, + }); + + delete Prism.languages.kotlin["class-name"]; + + const interpolationInside = { + "interpolation-punctuation": { + pattern: /^\$\{?|\}$/, + alias: "punctuation", + }, + expression: { + pattern: /[\s\S]+/, + inside: Prism.languages.kotlin, + }, + }; + + Prism.languages.insertBefore("kotlin", "string", { + // https://kotlinlang.org/spec/expressions.html#string-interpolation-expressions + "string-literal": [ + { + pattern: /"""(?:[^$]|\$(?:(?!\{)|\{[^{}]*\}))*?"""/, + alias: "multiline", + inside: { + interpolation: { + pattern: /\$(?:[a-z_]\w*|\{[^{}]*\})/i, + inside: interpolationInside, + }, + string: /[\s\S]+/, + }, + }, + { + pattern: /"(?:[^"\\\r\n$]|\\.|\$(?:(?!\{)|\{[^{}]*\}))*"/, + alias: "singleline", + inside: { + interpolation: { + pattern: /((?:^|[^\\])(?:\\{2})*)\$(?:[a-z_]\w*|\{[^{}]*\})/i, + lookbehind: true, + inside: interpolationInside, + }, + string: /[\s\S]+/, + }, + }, + ], + char: { + // https://kotlinlang.org/spec/expressions.html#character-literals + pattern: /'(?:[^'\\\r\n]|\\(?:.|u[a-fA-F0-9]{0,4}))'/, + greedy: true, + }, + }); + + delete Prism.languages.kotlin["string"]; + + Prism.languages.insertBefore("kotlin", "keyword", { + annotation: { + pattern: /\B@(?:\w+:)?(?:[A-Z]\w*|\[[^\]]+\])/, + alias: "builtin", + }, + }); + + Prism.languages.insertBefore("kotlin", "function", { + label: { + pattern: /\b\w+@|@\w+\b/, + alias: "symbol", + }, + }); + + Prism.languages.kt = Prism.languages.kotlin; + Prism.languages.kts = Prism.languages.kotlin; +} diff --git a/packages/react-notion-custom/src/lib/components/code/support-language/markdown.ts b/packages/react-notion-custom/src/lib/components/code/support-language/markdown.ts new file mode 100644 index 0000000..d5d3fde --- /dev/null +++ b/packages/react-notion-custom/src/lib/components/code/support-language/markdown.ts @@ -0,0 +1,468 @@ +/* eslint-disable prefer-const */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-useless-escape */ +let loaded = false; + +export default function load(Prism: any) { + if (loaded) return; + _load(Prism); + loaded = true; +} + +function _load(Prism: any) { + // Allow only one line break + let inner = /(?:\\.|[^\\\n\r]|(?:\n|\r\n?)(?![\r\n]))/.source; + + /** + * This function is intended for the creation of the bold or italic pattern. + * + * This also adds a lookbehind group to the given pattern to ensure that the pattern is not backslash-escaped. + * + * _Note:_ Keep in mind that this adds a capturing group. + * + * @param {string} pattern + * @returns {RegExp} + */ + function createInline(pattern: any) { + pattern = pattern.replace(//g, function () { + return inner; + }); + return RegExp(/((?:^|[^\\])(?:\\{2})*)/.source + "(?:" + pattern + ")"); + } + + let tableCell = /(?:\\.|``(?:[^`\r\n]|`(?!`))+``|`[^`\r\n]+`|[^\\|\r\n`])+/ + .source; + let tableRow = /\|?__(?:\|__)+\|?(?:(?:\n|\r\n?)|(?![\s\S]))/.source.replace( + /__/g, + function () { + return tableCell; + }, + ); + let tableLine = + /\|?[ \t]*:?-{3,}:?[ \t]*(?:\|[ \t]*:?-{3,}:?[ \t]*)+\|?(?:\n|\r\n?)/ + .source; + + Prism.languages.markdown = Prism.languages.extend("markup", {}); + Prism.languages.insertBefore("markdown", "prolog", { + "front-matter-block": { + pattern: /(^(?:\s*[\r\n])?)---(?!.)[\s\S]*?[\r\n]---(?!.)/, + lookbehind: true, + greedy: true, + inside: { + punctuation: /^---|---$/, + "front-matter": { + pattern: /\S+(?:\s+\S+)*/, + alias: ["yaml", "language-yaml"], + inside: Prism.languages.yaml, + }, + }, + }, + blockquote: { + // > ... + pattern: /^>(?:[\t ]*>)*/m, + alias: "punctuation", + }, + table: { + pattern: RegExp( + "^" + tableRow + tableLine + "(?:" + tableRow + ")*", + "m", + ), + inside: { + "table-data-rows": { + pattern: RegExp( + "^(" + tableRow + tableLine + ")(?:" + tableRow + ")*$", + ), + lookbehind: true, + inside: { + "table-data": { + pattern: RegExp(tableCell), + inside: Prism.languages.markdown, + }, + punctuation: /\|/, + }, + }, + "table-line": { + pattern: RegExp("^(" + tableRow + ")" + tableLine + "$"), + lookbehind: true, + inside: { + punctuation: /\||:?-{3,}:?/, + }, + }, + "table-header-row": { + pattern: RegExp("^" + tableRow + "$"), + inside: { + "table-header": { + pattern: RegExp(tableCell), + alias: "important", + inside: Prism.languages.markdown, + }, + punctuation: /\|/, + }, + }, + }, + }, + code: [ + { + // Prefixed by 4 spaces or 1 tab and preceded by an empty line + pattern: + /((?:^|\n)[ \t]*\n|(?:^|\r\n?)[ \t]*\r\n?)(?: {4}|\t).+(?:(?:\n|\r\n?)(?: {4}|\t).+)*/, + lookbehind: true, + alias: "keyword", + }, + { + // ```optional language + // code block + // ``` + pattern: /^```[\s\S]*?^```$/m, + greedy: true, + inside: { + "code-block": { + pattern: /^(```.*(?:\n|\r\n?))[\s\S]+?(?=(?:\n|\r\n?)^```$)/m, + lookbehind: true, + }, + "code-language": { + pattern: /^(```).+/, + lookbehind: true, + }, + punctuation: /```/, + }, + }, + ], + title: [ + { + // title 1 + // ======= + + // title 2 + // ------- + pattern: /\S.*(?:\n|\r\n?)(?:==+|--+)(?=[ \t]*$)/m, + alias: "important", + inside: { + punctuation: /==+$|--+$/, + }, + }, + { + // # title 1 + // ###### title 6 + pattern: /(^\s*)#.+/m, + lookbehind: true, + alias: "important", + inside: { + punctuation: /^#+|#+$/, + }, + }, + ], + hr: { + // *** + // --- + // * * * + // ----------- + pattern: /(^\s*)([*-])(?:[\t ]*\2){2,}(?=\s*$)/m, + lookbehind: true, + alias: "punctuation", + }, + list: { + // * item + // + item + // - item + // 1. item + pattern: /(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m, + lookbehind: true, + alias: "punctuation", + }, + "url-reference": { + // [id]: http://example.com "Optional title" + // [id]: http://example.com 'Optional title' + // [id]: http://example.com (Optional title) + // [id]: "Optional title" + pattern: + /!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/, + inside: { + letiable: { + pattern: /^(!?\[)[^\]]+/, + lookbehind: true, + }, + string: /(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/, + punctuation: /^[\[\]!:]|[<>]/, + }, + alias: "url", + }, + bold: { + // **strong** + // __strong__ + + // allow one nested instance of italic text using the same delimiter + pattern: createInline( + /\b__(?:(?!_)|_(?:(?!_))+_)+__\b|\*\*(?:(?!\*)|\*(?:(?!\*))+\*)+\*\*/ + .source, + ), + lookbehind: true, + greedy: true, + inside: { + content: { + pattern: /(^..)[\s\S]+(?=..$)/, + lookbehind: true, + inside: {}, // see below + }, + punctuation: /\*\*|__/, + }, + }, + italic: { + // *em* + // _em_ + + // allow one nested instance of bold text using the same delimiter + pattern: createInline( + /\b_(?:(?!_)|__(?:(?!_))+__)+_\b|\*(?:(?!\*)|\*\*(?:(?!\*))+\*\*)+\*/ + .source, + ), + lookbehind: true, + greedy: true, + inside: { + content: { + pattern: /(^.)[\s\S]+(?=.$)/, + lookbehind: true, + inside: {}, // see below + }, + punctuation: /[*_]/, + }, + }, + strike: { + // ~~strike through~~ + // ~strike~ + pattern: createInline(/(~~?)(?:(?!~))+\2/.source), + lookbehind: true, + greedy: true, + inside: { + content: { + pattern: /(^~~?)[\s\S]+(?=\1$)/, + lookbehind: true, + inside: {}, // see below + }, + punctuation: /~~?/, + }, + }, + "code-snippet": { + // `code` + // ``code`` + pattern: + /(^|[^\\`])(?:``[^`\r\n]+(?:`[^`\r\n]+)*``(?!`)|`[^`\r\n]+`(?!`))/, + lookbehind: true, + greedy: true, + alias: ["code", "keyword"], + }, + url: { + // [example](http://example.com "Optional title") + // [example][id] + // [example] [id] + pattern: createInline( + /!?\[(?:(?!\]))+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)|[ \t]?\[(?:(?!\]))+\])/ + .source, + ), + lookbehind: true, + greedy: true, + inside: { + operator: /^!/, + content: { + pattern: /(^\[)[^\]]+(?=\])/, + lookbehind: true, + inside: {}, // see below + }, + letiable: { + pattern: /(^\][ \t]?\[)[^\]]+(?=\]$)/, + lookbehind: true, + }, + url: { + pattern: /(^\]\()[^\s)]+/, + lookbehind: true, + }, + string: { + pattern: /(^[ \t]+)"(?:\\.|[^"\\])*"(?=\)$)/, + lookbehind: true, + }, + }, + }, + }); + + ["url", "bold", "italic", "strike"].forEach(function (token: any) { + ["url", "bold", "italic", "strike", "code-snippet"].forEach( + function (inside) { + if (token !== inside) { + Prism.languages.markdown[token].inside.content.inside[inside] = + Prism.languages.markdown[inside]; + } + }, + ); + }); + + Prism.hooks.add("after-tokenize", function (env: any) { + if (env.language !== "markdown" && env.language !== "md") { + return; + } + + function walkTokens(tokens: any) { + if (!tokens || typeof tokens === "string") { + return; + } + + for (let i = 0, l = tokens.length; i < l; i++) { + let token = tokens[i]; + + if (token.type !== "code") { + walkTokens(token.content); + continue; + } + + /* + * Add the correct `language-xxxx` class to this code block. Keep in mind that the `code-language` token + * is optional. But the grammar is defined so that there is only one case we have to handle: + * + * token.content = [ + * ```, + * xxxx, + * '\n', // exactly one new lines (\r or \n or \r\n) + * ..., + * '\n', // exactly one new lines again + * ``` + * ]; + */ + + let codeLang = token.content[1]; + let codeBlock = token.content[3]; + + if ( + codeLang && + codeBlock && + codeLang.type === "code-language" && + codeBlock.type === "code-block" && + typeof codeLang.content === "string" + ) { + // this might be a language that Prism does not support + + // do some replacements to support C++, C#, and F# + let lang = codeLang.content + .replace(/\b#/g, "sharp") + .replace(/\b\+\+/g, "pp"); + // only use the first word + lang = (/[a-z][\w-]*/i.exec(lang) || [""])[0].toLowerCase(); + let alias = "language-" + lang; + + // add alias + if (!codeBlock.alias) { + codeBlock.alias = [alias]; + } else if (typeof codeBlock.alias === "string") { + codeBlock.alias = [codeBlock.alias, alias]; + } else { + codeBlock.alias.push(alias); + } + } + } + } + + walkTokens(env.tokens); + }); + + Prism.hooks.add("wrap", function (env: any) { + if (env.type !== "code-block") { + return; + } + + let codeLang = ""; + for (let i = 0, l = env.classes.length; i < l; i++) { + let cls = env.classes[i]; + let match = /language-(.+)/.exec(cls); + if (match) { + codeLang = match[1]; + break; + } + } + + let grammar = Prism.languages[codeLang]; + + if (!grammar) { + if (codeLang && codeLang !== "none" && Prism.plugins.autoloader) { + let id = + "md-" + new Date().valueOf() + "-" + Math.floor(Math.random() * 1e16); + env.attributes["id"] = id; + + Prism.plugins.autoloader.loadLanguages(codeLang, function () { + let ele = document.getElementById(id); + if (ele) { + ele.innerHTML = Prism.highlight( + ele.textContent, + Prism.languages[codeLang], + codeLang, + ); + } + }); + } + } else { + env.content = Prism.highlight( + textContent(env.content), + grammar, + codeLang, + ); + } + }); + + let tagPattern = RegExp(Prism.languages.markup.tag.pattern.source, "gi"); + + /** + * A list of known entity names. + * + * This will always be incomplete to save space. The current list is the one used by lowdash's unescape function. + * + * @see {@link https://github.com/lodash/lodash/blob/2da024c3b4f9947a48517639de7560457cd4ec6c/unescape.js#L2} + */ + let KNOWN_ENTITY_NAMES = { + amp: "&", + lt: "<", + gt: ">", + quot: '"', + }; + + // IE 11 doesn't support `String.fromCodePoint` + let fromCodePoint = String.fromCodePoint || String.fromCharCode; + + /** + * Returns the text content of a given HTML source code string. + * + * @param {string} html + * @returns {string} + */ + function textContent(html: any) { + // remove all tags + let text = html.replace(tagPattern, ""); + + // decode known entities + text = text.replace( + /&(\w{1,8}|#x?[\da-f]{1,8});/gi, + function (m: any, code: any) { + code = code.toLowerCase(); + + if (code[0] === "#") { + let value; + if (code[1] === "x") { + value = parseInt(code.slice(2), 16); + } else { + value = Number(code.slice(1)); + } + + return fromCodePoint(value); + } else { + let known = (KNOWN_ENTITY_NAMES as any)[code]; + if (known) { + return known; + } + + // unable to decode + return m; + } + }, + ); + + return text; + } + + Prism.languages.md = Prism.languages.markdown; +} diff --git a/packages/react-notion-custom/src/lib/components/code/support-language/python.ts b/packages/react-notion-custom/src/lib/components/code/support-language/python.ts new file mode 100644 index 0000000..0a442b5 --- /dev/null +++ b/packages/react-notion-custom/src/lib/components/code/support-language/python.ts @@ -0,0 +1,83 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +let loaded = false; + +export default function load(Prism: any) { + if (loaded) return; + _load(Prism); + loaded = true; +} + +function _load(Prism: any) { + Prism.languages.python = { + comment: { + pattern: /(^|[^\\])#.*/, + lookbehind: true, + greedy: true, + }, + "string-interpolation": { + pattern: + /(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i, + greedy: true, + inside: { + interpolation: { + // "{" "}" + pattern: + /((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/, + lookbehind: true, + inside: { + "format-spec": { + pattern: /(:)[^:(){}]+(?=\}$)/, + lookbehind: true, + }, + "conversion-option": { + pattern: /![sra](?=[:}]$)/, + alias: "punctuation", + }, + rest: null, + }, + }, + string: /[\s\S]+/, + }, + }, + "triple-quoted-string": { + pattern: /(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i, + greedy: true, + alias: "string", + }, + string: { + pattern: /(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i, + greedy: true, + }, + function: { + pattern: /((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g, + lookbehind: true, + }, + "class-name": { + pattern: /(\bclass\s+)\w+/i, + lookbehind: true, + }, + decorator: { + pattern: /(^[\t ]*)@\w+(?:\.\w+)*/m, + lookbehind: true, + alias: ["annotation", "punctuation"], + inside: { + punctuation: /\./, + }, + }, + keyword: + /\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/, + builtin: + /\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/, + boolean: /\b(?:False|None|True)\b/, + number: + /\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i, + operator: /[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/, + punctuation: /[{}[\];(),.:]/, + }; + + Prism.languages.python["string-interpolation"].inside[ + "interpolation" + ].inside.rest = Prism.languages.python; + + Prism.languages.py = Prism.languages.python; +} diff --git a/packages/react-notion-custom/src/lib/components/code/support-language/sql.ts b/packages/react-notion-custom/src/lib/components/code/support-language/sql.ts new file mode 100644 index 0000000..ed65d44 --- /dev/null +++ b/packages/react-notion-custom/src/lib/components/code/support-language/sql.ts @@ -0,0 +1,47 @@ +/* eslint-disable no-useless-escape */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +let loaded = false; + +export default function load(Prism: any) { + if (loaded) return; + _load(Prism); + loaded = true; +} + +function _load(Prism: any) { + Prism.languages.sql = { + comment: { + pattern: /(^|[^\\])(?:\/\*[\s\S]*?\*\/|(?:--|\/\/|#).*)/, + lookbehind: true, + }, + constiable: [ + { + pattern: /@(["'`])(?:\\[\s\S]|(?!\1)[^\\])+\1/, + greedy: true, + }, + /@[\w.$]+/, + ], + string: { + pattern: /(^|[^@\\])("|')(?:\\[\s\S]|(?!\2)[^\\]|\2\2)*\2/, + greedy: true, + lookbehind: true, + }, + identifier: { + pattern: /(^|[^@\\])`(?:\\[\s\S]|[^`\\]|``)*`/, + greedy: true, + lookbehind: true, + inside: { + punctuation: /^`|`$/, + }, + }, + function: + /\b(?:AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\s*\()/i, // Should we highlight user defined functions too? + keyword: + /\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:COL|_INSERT)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|KEYS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:ING|S)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|const(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\b/i, + boolean: /\b(?:FALSE|NULL|TRUE)\b/i, + number: /\b0x[\da-f]+\b|\b\d+(?:\.\d*)?|\B\.\d+\b/i, + operator: + /[-+*\/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i, + punctuation: /[;[\]()`,.]/, + }; +} diff --git a/packages/react-notion-custom/src/lib/components/code/support-language/typescript.ts b/packages/react-notion-custom/src/lib/components/code/support-language/typescript.ts new file mode 100644 index 0000000..14045dc --- /dev/null +++ b/packages/react-notion-custom/src/lib/components/code/support-language/typescript.ts @@ -0,0 +1,71 @@ +/* eslint-disable no-useless-escape */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +let loaded = false; + +export default function load(Prism: any) { + if (loaded) return; + _load(Prism); + loaded = true; +} + +function _load(Prism: any) { + Prism.languages.typescript = Prism.languages.extend("javascript", { + "class-name": { + pattern: + /(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/, + lookbehind: true, + greedy: true, + inside: null, // see below + }, + builtin: + /\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/, + }); + + // The keywords TypeScript adds to JavaScript + Prism.languages.typescript.keyword.push( + /\b(?:abstract|declare|is|keyof|readonly|require)\b/, + // keywords that have to be followed by an identifier + /\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/, + // This is for `import type *, {}` + /\btype\b(?=\s*(?:[\{*]|$))/, + ); + + // doesn't work with TS because TS is too complex + delete Prism.languages.typescript["parameter"]; + delete Prism.languages.typescript["literal-property"]; + + // a version of typescript specifically for highlighting types + const typeInside = Prism.languages.extend("typescript", {}); + delete typeInside["class-name"]; + + Prism.languages.typescript["class-name"].inside = typeInside; + + Prism.languages.insertBefore("typescript", "function", { + decorator: { + pattern: /@[$\w\xA0-\uFFFF]+/, + inside: { + at: { + pattern: /^@/, + alias: "operator", + }, + function: /^[\s\S]+/, + }, + }, + "generic-function": { + // e.g. foo( ... + pattern: + /#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/, + greedy: true, + inside: { + function: /^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/, + generic: { + pattern: /<[\s\S]+/, // everything after the first < + alias: "class-name", + inside: typeInside, + }, + }, + }, + }); + + Prism.languages.ts = Prism.languages.typescript; +} diff --git a/packages/react-notion-custom/src/lib/components/index.ts b/packages/react-notion-custom/src/lib/components/index.ts index 7fbe816..38b0fea 100644 --- a/packages/react-notion-custom/src/lib/components/index.ts +++ b/packages/react-notion-custom/src/lib/components/index.ts @@ -10,6 +10,7 @@ import Divider from "./divider"; import Video from "./video"; import Column from "./column"; import ColumnList from "./column-list"; +import Code from "./code"; export { Headings, @@ -24,6 +25,7 @@ export { Video, Column, ColumnList, + Code, }; export default { @@ -41,4 +43,5 @@ export default { video: Video, column: Column, column_list: ColumnList, + code: Code, }; diff --git a/packages/react-notion-custom/src/lib/index.css b/packages/react-notion-custom/src/lib/index.css index 5c46d7d..ceb42ff 100644 --- a/packages/react-notion-custom/src/lib/index.css +++ b/packages/react-notion-custom/src/lib/index.css @@ -475,9 +475,11 @@ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; } + .notion-inline-underscore { text-decoration: underline; } + .notion-link { color: inherit; word-break: break-word; @@ -489,14 +491,17 @@ border-color 100ms ease-in, opacity 100ms ease-in; } + .notion-link:hover { border-color: var(--fg-color-6); opacity: 1; } + .notion-span { word-break: break-all; white-space: pre-wrap; } + .notion-equation { position: relative; display: inline-flex; @@ -506,14 +511,17 @@ border-radius: 3px; transition: background 20ms ease-in 0s; } + .notion-equation-inline { -webkit-user-select: all; -moz-user-select: all; user-select: all; } + .notion-equation:hover { background: var(--bg-color-0); } + .notion-equation:active, .notion-equation:focus { background: var(--select-color-2); @@ -534,6 +542,7 @@ .notion-h1-content { font-size: 1.875rem; } + .notion-h2:not(:last-child) { margin-bottom: 2px; } @@ -541,15 +550,19 @@ .notion-h2:not(:first-child) { margin-top: 1.62rem; } + .notion-h2-content { font-size: 1.5rem; } + .notion-h3:not(:last-child) { margin-bottom: 2px; } + .notion-h3:not(:first-child) { margin-top: 1.25rem; } + .notion-h3-content { font-size: 1.25rem; } @@ -560,6 +573,7 @@ margin-block-start: 0; margin-block-end: 0; } + .notion-paragraph-content { padding: 3px 2px; } @@ -584,9 +598,11 @@ margin: 2px 0; cursor: pointer; } + .notion-equation:hover { background: var(--bg-color-0); } + .notion-equation:active, .notion-equation:focus { background: var(--select-color-2); @@ -713,6 +729,138 @@ flex-direction: column; } +.notion-code { + position: relative; + font-size: 85%; +} + +.notion-code code { + white-space: pre-wrap !important; +} + +.notion-code-content { + tab-size: 2; + padding: 2.5em; + background: var(--bg-color-1); + border-radius: 3px; + display: block; + box-sizing: border-box; + overflow: auto; + font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, + monospace; +} + +.notion-code-language { + font-size: 12px; + caret-color: var(--fg-color); + color: var(--fg-color-3); + background-color: var(--fg-color-0); + user-select: none; + z-index: 9; + transition: opacity 0.4s cubic-bezier(0.3, 0, 0.5, 1); + padding: 0px 6px; + border-radius: 2px; +} + +.notion-code-meta { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px; + position: absolute; + top: 0; + left: 0; + width: 100%; + transition: opacity 0.3s cubic-bezier(0.3, 0, 0.5, 1); +} + +.notion-code-copy { + user-select: none; + z-index: 9; + display: flex; + padding: 6px 4px; + align-items: center; + font-size: 12px; + line-height: 1em; + cursor: pointer; + gap: 4px; + caret-color: var(--fg-color); + color: var(--fg-color-3); + background-color: var(--fg-color-0); + + transition: + background-color 0.2s cubic-bezier(0.3, 0, 0.5, 1), + color 0.2s cubic-bezier(0.3, 0, 0.5, 1), + border-color 0.2s cubic-bezier(0.3, 0, 0.5, 1); + box-shadow: + 0 1px 0 rgba(27, 31, 36, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.25); + + color: #24292f; + border: none; + border-radius: 2px; +} + +.notion-code-copy:hover { + background-color: var(--fg-color-1); + border-color: rgba(27, 31, 36, 0.15); + transition-duration: 0.1s; +} + +.notion-code-copy:active { + background: hsla(220, 14%, 93%, 1); + border-color: rgba(27, 31, 36, 0.15); + transition: none; +} + +.notion-code .notion-code-meta { + opacity: 0; +} + +.notion-code:hover .notion-code-meta { + opacity: 1; +} + +.notion-code-copy-tooltip { + pointer-events: none; + position: fixed; + bottom: 16px; + left: 0; + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + z-index: 99; + font-size: 14px; + transition: + opacity 0.3s, + transform 0.3s; +} + +.notion-code-copy-tooltip > div { + padding: 6px 8px; + background: #222; + color: #fff; + border-radius: 6px; +} + +.notion-code-copy-tooltip > div { + padding: 6px 8px; + background: #222; + color: #fff; + border-radius: 6px; +} + +.notion-code-copy-tooltip.notion-visible { + opacity: 1; + transform: translateY(0); +} + +.notion-code-copy-tooltip.notion-hidden { + opacity: 0; + transform: translateY(120%); +} + .notion-column-list { display: grid; grid-template-columns: repeat( diff --git a/packages/story/src/stories/code/code.json b/packages/story/src/stories/code/code.json new file mode 100644 index 0000000..aed7d1a --- /dev/null +++ b/packages/story/src/stories/code/code.json @@ -0,0 +1,352 @@ +{ + "object": "page", + "id": "591d29d3-cb19-4273-bb3a-82644ed4faa4", + "created_time": "2022-12-28T23:30:00.000Z", + "last_edited_time": "2023-01-07T04:15:00.000Z", + "created_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "last_edited_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "cover": { + "type": "external", + "external": { + "url": "https://www.notion.so/images/page-cover/solid_beige.png" + } + }, + "icon": null, + "parent": { + "type": "database_id", + "database_id": "be65d799-9e98-4426-86a6-72072991e27b" + }, + "archived": false, + "properties": { + "HashTags": { + "id": "Hhkx", + "type": "multi_select", + "multi_select": [] + }, + "생성 일시": { + "id": "J%7C%3BZ", + "type": "created_time", + "created_time": "2022-12-28T23:30:00.000Z" + }, + "Slug": { + "id": "S%3A%7B%3E", + "type": "rich_text", + "rich_rich_text": [ + { + "type": "text", + "text": { + "content": "test", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "test", + "href": null + } + ] + }, + "Description": { + "id": "qTV%3E", + "type": "rich_text", + "rich_rich_text": [] + }, + "Status": { + "id": "vu%7C%3B", + "type": "select", + "select": { + "id": "|QrX", + "name": "Publishable", + "color": "green" + } + }, + "Name": { + "id": "title", + "type": "title", + "title": [ + { + "type": "text", + "text": { + "content": "테스트 paragrap", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "테스트 paragrap", + "href": null + } + ] + } + }, + "url": "https://www.notion.so/paragrap-591d29d3cb194273bb3a82644ed4faa4", + "blocks": [ + { + "object": "block", + "id": "06d7c6ab-cb38-4039-bde8-fec3b85ea728", + "parent": { + "type": "page_id", + "page_id": "591d29d3-cb19-4273-bb3a-82644ed4faa4" + }, + "created_time": "2023-01-07T04:10:00.000Z", + "last_edited_time": "2023-01-07T04:10:00.000Z", + "created_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "last_edited_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "color": "gray_background", + "rich_text": [] + } + }, + { + "object": "block", + "id": "e2421c70-1874-4ea7-b7be-16902d4d0dd1", + "parent": { + "type": "page_id", + "page_id": "591d29d3-cb19-4273-bb3a-82644ed4faa4" + }, + "created_time": "2023-01-07T04:07:00.000Z", + "last_edited_time": "2023-01-07T04:15:00.000Z", + "created_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "last_edited_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "has_children": false, + "archived": false, + "type": "code", + "code": { + "caption": [ + { + "type": "text", + "text": { + "content": "it’s code block caption", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "it’s code block caption", + "href": null + } + ], + "language": "javascript", + "rich_text": [ + { + "type": "text", + "text": { + "content": "// it's javascript code block\nfunction sum(n) {\n\tif(n == 1) return 1;\n\tif(n % 2 == 1) return n + sum(n-1)\n\treturn Math.pow(n/2, 2) + 2 * sum(n / 2)\n}", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "// it's javascript code block\nfunction sum(n) {\n\tif(n == 1) return 1;\n\tif(n % 2 == 1) return n + sum(n-1)\n\treturn Math.pow(n/2, 2) + 2 * sum(n / 2)\n}", + "href": null + } + ] + } + }, + { + "object": "block", + "id": "e77001a4-9b1b-40ea-9349-a73d022e7d87", + "parent": { + "type": "page_id", + "page_id": "591d29d3-cb19-4273-bb3a-82644ed4faa4" + }, + "created_time": "2023-01-07T03:39:00.000Z", + "last_edited_time": "2023-01-07T04:10:00.000Z", + "created_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "last_edited_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "color": "gray_background", + "rich_text": [] + } + }, + { + "object": "block", + "id": "f938d548-9cdb-4250-8f51-2b49af2598f8", + "parent": { + "type": "page_id", + "page_id": "591d29d3-cb19-4273-bb3a-82644ed4faa4" + }, + "created_time": "2023-01-07T04:08:00.000Z", + "last_edited_time": "2023-01-07T04:14:00.000Z", + "created_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "last_edited_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "has_children": true, + "archived": false, + "type": "paragraph", + "paragraph": { + "color": "gray_background", + "rich_text": [] + }, + "blocks": [ + { + "object": "block", + "id": "4f450777-4131-4750-ae22-fa36f3bd7169", + "parent": { + "type": "block_id", + "block_id": "f938d548-9cdb-4250-8f51-2b49af2598f8" + }, + "created_time": "2023-01-07T03:39:00.000Z", + "last_edited_time": "2023-01-07T04:10:00.000Z", + "created_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "last_edited_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "color": "blue_background", + "rich_text": [ + { + "type": "text", + "text": { + "content": "nest paragraph", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "nest paragraph", + "href": null + } + ] + } + }, + { + "object": "block", + "id": "09a029fa-1c75-46fb-a9ed-646a02121255", + "parent": { + "type": "block_id", + "block_id": "f938d548-9cdb-4250-8f51-2b49af2598f8" + }, + "created_time": "2023-01-07T04:09:00.000Z", + "last_edited_time": "2023-01-07T04:15:00.000Z", + "created_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "last_edited_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "has_children": false, + "archived": false, + "type": "code", + "code": { + "caption": [], + "language": "javascript", + "rich_text": [ + { + "type": "text", + "text": { + "content": "// it's nest code block\nfunction sum(n) {\n if(n == 1) return 1;\n return n + sum(n - 1)\n}", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "// it's nest code block\nfunction sum(n) {\n if(n == 1) return 1;\n return n + sum(n - 1)\n}", + "href": null + } + ] + } + }, + { + "object": "block", + "id": "8d5488ff-17aa-4a8c-bc05-ee6ca078e522", + "parent": { + "type": "block_id", + "block_id": "f938d548-9cdb-4250-8f51-2b49af2598f8" + }, + "created_time": "2023-01-07T04:10:00.000Z", + "last_edited_time": "2023-01-07T04:15:00.000Z", + "created_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "last_edited_by": { + "object": "user", + "id": "95fc0174-8fc6-4114-8e45-f67eacd99f07" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "color": "blue_background", + "rich_text": [] + } + } + ] + } + ] +} diff --git a/packages/story/src/stories/code/code.stories.tsx b/packages/story/src/stories/code/code.stories.tsx new file mode 100644 index 0000000..65f995a --- /dev/null +++ b/packages/story/src/stories/code/code.stories.tsx @@ -0,0 +1,102 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import Component from "../../lib/Notion"; +import json from "./code.json"; +import { getCodeExampleJson } from "./getCodeExampleJson"; +import { + java, + javascript, + dart, + kotlin, + typescript, + markdown, + python, + sql, + elixir, + go, +} from "./support-code-examples"; + +const blocks = json.blocks as any; + +const meta: Meta = { + title: "Blocks/Code", + component: Component, +}; + +export default meta; +type Story = StoryObj; + +export const Code: Story = { + args: { + title: "Code", + blocks: blocks, + }, +}; +export const Dart: Story = { + args: { + title: "Dart", + blocks: getCodeExampleJson(dart, "dart"), + }, +}; + +export const Java: Story = { + args: { + title: "Java", + blocks: getCodeExampleJson(java, "java"), + }, +}; + +export const Javascript: Story = { + args: { + title: "Javascript", + blocks: getCodeExampleJson(javascript, "javascript"), + }, +}; + +export const Kotlin: Story = { + args: { + title: "Kotlin", + blocks: getCodeExampleJson(kotlin, "kotlin"), + }, +}; + +export const Typescript: Story = { + args: { + title: "Typescript", + blocks: getCodeExampleJson(typescript, "typescript"), + }, +}; + +export const Markdown: Story = { + args: { + title: "Markdown", + blocks: getCodeExampleJson(markdown, "markdown"), + }, +}; + +export const Python: Story = { + args: { + title: "Python", + blocks: getCodeExampleJson(python, "python"), + }, +}; + +export const Sql: Story = { + args: { + title: "Sql", + blocks: getCodeExampleJson(sql, "sql"), + }, +}; + +export const Go: Story = { + args: { + title: "Go", + blocks: getCodeExampleJson(go, "go"), + }, +}; + +export const Elixir: Story = { + args: { + title: "Elixir", + blocks: getCodeExampleJson(elixir, "elixir"), + }, +}; diff --git a/packages/story/src/stories/code/getCodeExampleJson.ts b/packages/story/src/stories/code/getCodeExampleJson.ts new file mode 100644 index 0000000..da74b0d --- /dev/null +++ b/packages/story/src/stories/code/getCodeExampleJson.ts @@ -0,0 +1,32 @@ +export function getCodeExampleJson(code: string, language: string) { + return [ + { + object: "block", + type: "code", + id: "id", + code: { + caption: [], + language: language, + rich_text: [ + { + type: "text", + text: { + content: code, + link: null, + }, + annotations: { + bold: false, + italic: false, + strikethrough: false, + underline: false, + code: false, + color: "default", + }, + plain_text: code, + href: null, + }, + ], + }, + }, + ] as any; +} diff --git a/packages/story/src/stories/code/support-code-examples/dart.ts b/packages/story/src/stories/code/support-code-examples/dart.ts new file mode 100644 index 0000000..268d8cf --- /dev/null +++ b/packages/story/src/stories/code/support-code-examples/dart.ts @@ -0,0 +1,61 @@ +import { dedent } from "ts-dedent"; +export const dart = dedent` +import 'package:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Counter App', + home: CounterScreen(), + ); + } +} + +class CounterScreen extends StatefulWidget { + @override + _CounterScreenState createState() => _CounterScreenState(); +} + +class _CounterScreenState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Flutter Counter App'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.headline4, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: Icon(Icons.add), + ), + ); + } +} +`; diff --git a/packages/story/src/stories/code/support-code-examples/elixir.ts b/packages/story/src/stories/code/support-code-examples/elixir.ts new file mode 100644 index 0000000..6e8e461 --- /dev/null +++ b/packages/story/src/stories/code/support-code-examples/elixir.ts @@ -0,0 +1,29 @@ +import { dedent } from "ts-dedent"; +export const elixir = dedent` +defmodule ChatServer do + def start(port \\ 4040) do + {:ok, listen_socket} = :gen_tcp.listen(port, [:binary, active: false, reuseaddr: true]) + Logger.info("Chat server started on port #{port}") + + accept_loop(listen_socket) + end + + def accept_loop(listen_socket) do + {:ok, client_socket} = :gen_tcp.accept(listen_socket) + spawn(fn -> handle_client(client_socket) end) + accept_loop(listen_socket) + end + + def handle_client(client_socket) do + {:ok, message} = :gen_tcp.recv(client_socket, 0) + Logger.info("Received message from client: #{message}") + broadcast(message) + + handle_client(client_socket) + end + + def broadcast(message) do + :inet.getaddr("localhost") |> Tuple.to_list() |> List.delete_at(-1) |> Enum.join(".") |> IO.puts(message) + end +end +`; diff --git a/packages/story/src/stories/code/support-code-examples/go.ts b/packages/story/src/stories/code/support-code-examples/go.ts new file mode 100644 index 0000000..9cc854b --- /dev/null +++ b/packages/story/src/stories/code/support-code-examples/go.ts @@ -0,0 +1,76 @@ +import { dedent } from "ts-dedent"; +export const go = dedent` +package main + +import "fmt" + +type Maze struct { + maze [][]int + visited [][]bool + rows int + cols int +} + +func NewMaze(maze [][]int) *Maze { + rows := len(maze) + cols := len(maze[0]) + + visited := make([][]bool, rows) + for i := range visited { + visited[i] = make([]bool, cols) + } + + return &Maze{ + maze: maze, + visited: visited, + rows: rows, + cols: cols, + } +} + +func (m *Maze) DFS(x, y int, path []string) []string { + if x < 0 || x >= m.rows || y < 0 || y >= m.cols { + return nil + } + + if m.visited[x][y] || m.maze[x][y] == 1 { + return nil + } + + path = append(path, fmt.Sprintf("(%d,%d)", x, y)) + + if x == m.rows-1 && y == m.cols-1 { + return path + } + + m.visited[x][y] = true + + if p := m.DFS(x+1, y, path); p != nil { + return p + } + if p := m.DFS(x, y+1, path); p != nil { + return p + } + if p := m.DFS(x-1, y, path); p != nil { + return p + } + if p := m.DFS(x, y-1, path); p != nil { + return p + } + + return nil +} + +func main() { + maze := [][]int{ + {0, 1, 1, 1}, + {0, 0, 1, 0}, + {1, 0, 0, 0}, + {1, 1, 0, 0}, + } + + m := NewMaze(maze) + path := m.DFS(0, 0, []string{}) + fmt.Println(path) +} +`; diff --git a/packages/story/src/stories/code/support-code-examples/index.ts b/packages/story/src/stories/code/support-code-examples/index.ts new file mode 100644 index 0000000..131fc7d --- /dev/null +++ b/packages/story/src/stories/code/support-code-examples/index.ts @@ -0,0 +1,10 @@ +export * from "./dart"; +export * from "./java"; +export * from "./javascript"; +export * from "./kotlin"; +export * from "./typescript"; +export * from "./markdown"; +export * from "./python"; +export * from "./sql"; +export * from "./go"; +export * from "./elixir"; diff --git a/packages/story/src/stories/code/support-code-examples/java.ts b/packages/story/src/stories/code/support-code-examples/java.ts new file mode 100644 index 0000000..6edca40 --- /dev/null +++ b/packages/story/src/stories/code/support-code-examples/java.ts @@ -0,0 +1,34 @@ +import { dedent } from "ts-dedent"; + +export const java = dedent` +public class Animal { + private String name; + private int age; + + public Animal(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } +} + +public class Cat extends Animal { + private String color; + + public Cat(String name, int age, String color) { + super(name, age); + this.color = color; + } + + public String getColor() { + return color; + } +} +`; diff --git a/packages/story/src/stories/code/support-code-examples/javascript.ts b/packages/story/src/stories/code/support-code-examples/javascript.ts new file mode 100644 index 0000000..ea707a3 --- /dev/null +++ b/packages/story/src/stories/code/support-code-examples/javascript.ts @@ -0,0 +1,28 @@ +import { dedent } from "ts-dedent"; +export const javascript = dedent` +class Animal { + constructor(name, age) { + this.name = name; + this.age = age; + } + + getName() { + return this.name; + } + + getAge() { + return this.age; + } +} + +class Cat extends Animal { + constructor(name, age, color) { + super(name, age); + this.color = color; + } + + getColor() { + return this.color; + } +} +`; diff --git a/packages/story/src/stories/code/support-code-examples/kotlin.ts b/packages/story/src/stories/code/support-code-examples/kotlin.ts new file mode 100644 index 0000000..06272eb --- /dev/null +++ b/packages/story/src/stories/code/support-code-examples/kotlin.ts @@ -0,0 +1,27 @@ +import { dedent } from "ts-dedent"; +export const kotlin = dedent` +data class Post(val id: Int, val title: String, val content: String, val type: PostType) + +enum class PostType { + TEXT, + IMAGE, + VIDEO +} + +fun processPost(post: Post) { + when (post.type) { + PostType.TEXT -> { + println("Processing text post: \${post.title}") + // Text post processing logic goes here + } + PostType.IMAGE -> { + println("Processing image post: \${post.title}") + // Image post processing logic goes here + } + PostType.VIDEO -> { + println("Processing video post: \${post.title}") + // Video post processing logic goes here + } + } +} +`; diff --git a/packages/story/src/stories/code/support-code-examples/markdown.ts b/packages/story/src/stories/code/support-code-examples/markdown.ts new file mode 100644 index 0000000..faea3ed --- /dev/null +++ b/packages/story/src/stories/code/support-code-examples/markdown.ts @@ -0,0 +1,38 @@ +import { dedent } from "ts-dedent"; + +export const markdown = dedent` +# My Document + +This is a paragraph of text. + +## Section 1 + +This is the first section of the document. It contains some **bold** text and some _italic_ text. + +### Subsection 1.1 + +This is a subsection of section 1. It contains a list: + +- Item 1 +- Item 2 +- Item 3 + +### Subsection 1.2 + +This is another subsection of section 1. It contains a table: + +| Name | Age | +| -------- | --- | +| Alice | 30 | +| Bob | 25 | +| Charlie | 35 | + +## Section 2 + +This is the second section of the document. It contains a code block: + +\`\`\`python +def hello(): + print("Hello, world!") +\`\`\` +`; diff --git a/packages/story/src/stories/code/support-code-examples/python.ts b/packages/story/src/stories/code/support-code-examples/python.ts new file mode 100644 index 0000000..cd1ef35 --- /dev/null +++ b/packages/story/src/stories/code/support-code-examples/python.ts @@ -0,0 +1,26 @@ +import { dedent } from "ts-dedent"; + +export const python = dedent` +import gym + +# Create the environment +env = gym.make('CartPole-v1') + +# Set up the agent +def policy(observation): + position, velocity, angle, angle_velocity = observation + action = 0 if angle < 0 else 1 + return action + +# Train the agent +total_reward = 0 +observation = env.reset() +for i in range(200): + action = policy(observation) + observation, reward, done, info = env.step(action) + total_reward += reward + if done: + break + +print(f'Total reward: {total_reward}') +`; diff --git a/packages/story/src/stories/code/support-code-examples/sql.ts b/packages/story/src/stories/code/support-code-examples/sql.ts new file mode 100644 index 0000000..115e893 --- /dev/null +++ b/packages/story/src/stories/code/support-code-examples/sql.ts @@ -0,0 +1,26 @@ +import { dedent } from "ts-dedent"; +export const sql = dedent` +-- Create the movies table +CREATE TABLE movies ( + id INTEGER PRIMARY KEY, + title TEXT, + release_year INTEGER, + director TEXT, + genre TEXT +); + +-- Insert some data +INSERT INTO movies (id, title, release_year, director, genre) +VALUES (1, 'The Shawshank Redemption', 1994, 'Frank Darabont', 'Drama'), + (2, 'The Godfather', 1972, 'Francis Ford Coppola', 'Drama'), + (3, 'The Dark Knight', 2008, 'Christopher Nolan', 'Action'), + (4, '12 Angry Men', 1957, 'Sidney Lumet', 'Drama'), + (5, 'Schindler''s List', 1993, 'Steven Spielberg', 'Drama'); + +-- Search for movies +SELECT title, release_year, director, genre +FROM movies +WHERE release_year >= 1990 + AND genre = 'Drama' +ORDER BY release_year DESC; +`; diff --git a/packages/story/src/stories/code/support-code-examples/typescript.ts b/packages/story/src/stories/code/support-code-examples/typescript.ts new file mode 100644 index 0000000..81884db --- /dev/null +++ b/packages/story/src/stories/code/support-code-examples/typescript.ts @@ -0,0 +1,33 @@ +import { dedent } from "ts-dedent"; +export const typescript = dedent` +interface Comparable { + compareTo(other: T): number; +} + +function max>(arr: T[]): T { + let max = arr[0]; + for (let i = 1; i < arr.length; i++) { + if (arr[i].compareTo(max) > 0) { + max = arr[i]; + } + } + return max; +} + +class Person implements Comparable { + constructor(public name: string, public age: number) {} + + compareTo(other: Person) { + return this.age - other.age; + } +} + +const people: Person[] = [ + new Person("Alice", 30), + new Person("Bob", 25), + new Person("Charlie", 35) +]; + +const oldestPerson = max(people); +console.log(\`The oldest person is \${oldestPerson.name}, who is \${oldestPerson.age} years old.\`); // Output: The oldest person is Charlie, who is 35 years old. +`;