diff --git a/apps/web/components/tailwind/advanced-editor.tsx b/apps/web/components/tailwind/advanced-editor.tsx index abc1b56e8..229a74720 100644 --- a/apps/web/components/tailwind/advanced-editor.tsx +++ b/apps/web/components/tailwind/advanced-editor.tsx @@ -1,29 +1,29 @@ "use client"; import { defaultEditorContent } from "@/lib/content"; -import React, { useEffect, useState } from "react"; -import { useDebouncedCallback } from "use-debounce"; import { - EditorRoot, EditorCommand, - EditorCommandItem, EditorCommandEmpty, + EditorCommandItem, + EditorCommandList, EditorContent, - type JSONContent, EditorInstance, - EditorCommandList, + EditorRoot, + type JSONContent, } from "novel"; import { ImageResizer, handleCommandNavigation } from "novel/extensions"; +import { useEffect, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; import { defaultExtensions } from "./extensions"; -import { Separator } from "./ui/separator"; -import { NodeSelector } from "./selectors/node-selector"; -import { LinkSelector } from "./selectors/link-selector"; import { ColorSelector } from "./selectors/color-selector"; +import { LinkSelector } from "./selectors/link-selector"; +import { NodeSelector } from "./selectors/node-selector"; +import { Separator } from "./ui/separator"; -import { TextButtons } from "./selectors/text-buttons"; -import { slashCommand, suggestionItems } from "./slash-command"; -import GenerativeMenuSwitch from "./generative/generative-menu-switch"; import { handleImageDrop, handleImagePaste } from "novel/plugins"; +import GenerativeMenuSwitch from "./generative/generative-menu-switch"; import { uploadFn } from "./image-upload"; +import { TextButtons } from "./selectors/text-buttons"; +import { slashCommand, suggestionItems } from "./slash-command"; const extensions = [...defaultExtensions, slashCommand]; @@ -41,8 +41,12 @@ const TailwindAdvancedEditor = () => { const debouncedUpdates = useDebouncedCallback( async (editor: EditorInstance) => { const json = editor.getJSON(); - + window.localStorage.setItem("html-content", editor.getHTML()); window.localStorage.setItem("novel-content", JSON.stringify(json)); + window.localStorage.setItem( + "markdown", + editor.storage.markdown.getMarkdown(), + ); setSaveStatus("Saved"); }, 500, diff --git a/apps/web/components/tailwind/extensions.ts b/apps/web/components/tailwind/extensions.ts index 9adc0962d..75f6b41a8 100644 --- a/apps/web/components/tailwind/extensions.ts +++ b/apps/web/components/tailwind/extensions.ts @@ -1,17 +1,19 @@ import { + AIHighlight, + CodeBlockLowlight, + HorizontalRule, + Placeholder, + StarterKit, + TaskItem, + TaskList, TiptapImage, TiptapLink, UpdatedImage, - TaskList, - TaskItem, - HorizontalRule, - StarterKit, - Placeholder, - AIHighlight, } from "novel/extensions"; import { UploadImagesPlugin } from "novel/plugins"; import { cx } from "class-variance-authority"; +import { common, createLowlight } from "lowlight"; //TODO I am using cx here to get tailwind autocomplete working, idk if someone else can write a regex to just capture the class key in objects const aiHighlight = AIHighlight; @@ -106,6 +108,12 @@ const starterKit = StarterKit.configure({ gapcursor: false, }); +const codeBlockLowlight = CodeBlockLowlight.configure({ + // configure lowlight: common / all / use highlightJS in case there is a need to specify certain language grammars only + // common: covers 37 language grammars which should be good enough in most cases + lowlight: createLowlight(common), +}); + export const defaultExtensions = [ starterKit, placeholder, @@ -116,4 +124,5 @@ export const defaultExtensions = [ taskItem, horizontalRule, aiHighlight, + codeBlockLowlight, ]; diff --git a/apps/web/package.json b/apps/web/package.json index 12d63f915..f6c9f3377 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -30,6 +30,8 @@ "eslint": "8.36.0", "eslint-config-next": "13.2.4", "eventsource-parser": "^0.1.0", + "highlight.js": "^11.9.0", + "lowlight": "^3.1.0", "lucide-react": "^0.244.0", "next": "14.1.0", "next-themes": "^0.2.1", diff --git a/apps/web/styles/globals.css b/apps/web/styles/globals.css index f744c33ce..637377cda 100644 --- a/apps/web/styles/globals.css +++ b/apps/web/styles/globals.css @@ -94,3 +94,71 @@ @apply bg-background text-foreground; } } + + +pre { + background: #0d0d0d; + border-radius: 0.5rem; + color: #fff; + font-family: "JetBrainsMono", monospace; + padding: 0.75rem 1rem; + + code { + background: none; + color: inherit; + font-size: 0.8rem; + padding: 0; + } + + .hljs-comment, + .hljs-quote { + color: #616161; + } + + .hljs-variable, + .hljs-template-variable, + .hljs-attribute, + .hljs-tag, + .hljs-name, + .hljs-regexp, + .hljs-link, + .hljs-name, + .hljs-selector-id, + .hljs-selector-class { + color: #f98181; + } + + .hljs-number, + .hljs-meta, + .hljs-built_in, + .hljs-builtin-name, + .hljs-literal, + .hljs-type, + .hljs-params { + color: #fbbc88; + } + + .hljs-string, + .hljs-symbol, + .hljs-bullet { + color: #b9f18d; + } + + .hljs-title, + .hljs-section { + color: #faf594; + } + + .hljs-keyword, + .hljs-selector-tag { + color: #70cff8; + } + + .hljs-emphasis { + font-style: italic; + } + + .hljs-strong { + font-weight: 700; + } +} diff --git a/packages/headless/package.json b/packages/headless/package.json index e969bc6db..6ff3eb4a1 100644 --- a/packages/headless/package.json +++ b/packages/headless/package.json @@ -40,6 +40,7 @@ "dependencies": { "@radix-ui/react-slot": "^1.0.2", "@tiptap/core": "^2.1.7", + "@tiptap/extension-code-block-lowlight": "^2.2.6", "@tiptap/extension-color": "^2.1.7", "@tiptap/extension-highlight": "^2.1.7", "@tiptap/extension-horizontal-rule": "^2.1.7", diff --git a/packages/headless/src/extensions/index.ts b/packages/headless/src/extensions/index.ts index 03f7885cf..5b27cb6c1 100644 --- a/packages/headless/src/extensions/index.ts +++ b/packages/headless/src/extensions/index.ts @@ -1,21 +1,24 @@ -import StarterKit from "@tiptap/starter-kit"; +import { InputRule } from "@tiptap/core"; +import { Color } from "@tiptap/extension-color"; +import Highlight from "@tiptap/extension-highlight"; import HorizontalRule from "@tiptap/extension-horizontal-rule"; -import TiptapLink from "@tiptap/extension-link"; import TiptapImage from "@tiptap/extension-image"; +import TiptapLink from "@tiptap/extension-link"; import Placeholder from "@tiptap/extension-placeholder"; -import TiptapUnderline from "@tiptap/extension-underline"; -import TextStyle from "@tiptap/extension-text-style"; -import { Color } from "@tiptap/extension-color"; import { TaskItem } from "@tiptap/extension-task-item"; import { TaskList } from "@tiptap/extension-task-list"; -import { InputRule } from "@tiptap/core"; +import TextStyle from "@tiptap/extension-text-style"; +import TiptapUnderline from "@tiptap/extension-underline"; +import StarterKit from "@tiptap/starter-kit"; +import AutoJoiner from "tiptap-extension-auto-joiner"; +import GlobalDragHandle from "tiptap-extension-global-drag-handle"; import { Markdown } from "tiptap-markdown"; -import Highlight from "@tiptap/extension-highlight"; -import UpdatedImage from "./updated-image"; import CustomKeymap from "./custom-keymap"; import { ImageResizer } from "./image-resizer"; -import GlobalDragHandle from "tiptap-extension-global-drag-handle"; -import AutoJoiner from "tiptap-extension-auto-joiner"; +import UpdatedImage from "./updated-image"; + +import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; + const PlaceholderExtension = Placeholder.configure({ placeholder: ({ node }) => { @@ -68,21 +71,23 @@ const Horizontal = HorizontalRule.extend({ }, }); + +export * from "./ai-highlight"; +export * from "./slash-command"; export { + CodeBlockLowlight, + Horizontal as HorizontalRule, + ImageResizer, + InputRule, PlaceholderExtension as Placeholder, - simpleExtensions, StarterKit, - Horizontal as HorizontalRule, - TiptapLink, - TiptapImage, - UpdatedImage, TaskItem, TaskList, - InputRule, - ImageResizer, + TiptapImage, + TiptapLink, + UpdatedImage, + simpleExtensions, }; -export * from "./ai-highlight"; -export * from "./slash-command"; // Todo: Maybe I should create an utils entry export { getPrevText } from "../utils/utils"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 025c05b4f..ae5a531ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,12 @@ importers: eventsource-parser: specifier: ^0.1.0 version: 0.1.0 + highlight.js: + specifier: ^11.9.0 + version: 11.9.0 + lowlight: + specifier: ^3.1.0 + version: 3.1.0 lucide-react: specifier: ^0.244.0 version: 0.244.0(react@18.2.0) @@ -159,6 +165,9 @@ importers: '@tiptap/core': specifier: ^2.1.7 version: 2.2.2(@tiptap/pm@2.2.2) + '@tiptap/extension-code-block-lowlight': + specifier: ^2.2.6 + version: 2.2.6(@tiptap/core@2.2.2)(@tiptap/extension-code-block@2.2.2)(@tiptap/pm@2.2.2) '@tiptap/extension-color': specifier: ^2.1.7 version: 2.2.2(@tiptap/core@2.2.2)(@tiptap/extension-text-style@2.2.2) @@ -2081,6 +2090,18 @@ packages: '@tiptap/core': 2.2.2(@tiptap/pm@2.2.2) dev: false + /@tiptap/extension-code-block-lowlight@2.2.6(@tiptap/core@2.2.2)(@tiptap/extension-code-block@2.2.2)(@tiptap/pm@2.2.2): + resolution: {integrity: sha512-W/8C5nIwgGLvxjc+PfnCcWkfrUuJsIKjyZGXmq1hVXTTVA9eVGbS7m1YB/fsYTEg1ccwoM2JjKO9yuKCeR2xiQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/extension-code-block': ^2.0.0 + '@tiptap/pm': ^2.0.0 + dependencies: + '@tiptap/core': 2.2.2(@tiptap/pm@2.2.2) + '@tiptap/extension-code-block': 2.2.2(@tiptap/core@2.2.2)(@tiptap/pm@2.2.2) + '@tiptap/pm': 2.2.2 + dev: false + /@tiptap/extension-code-block@2.2.2(@tiptap/core@2.2.2)(@tiptap/pm@2.2.2): resolution: {integrity: sha512-CKn4xqhpCfwkVdkj//A+LVf0hFrRkBbDx8u3KG+I7cegjXxvDSqb2OGhn/tXpFatLAE50GJiPIvqf+TmhIWBvA==} peerDependencies: @@ -2398,6 +2419,12 @@ packages: '@types/unist': 2.0.10 dev: false + /@types/hast@3.0.4: + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + dependencies: + '@types/unist': 2.0.10 + dev: false + /@types/istanbul-lib-coverage@2.0.6: resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} dev: false @@ -3512,6 +3539,12 @@ packages: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} dev: false + /devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dependencies: + dequal: 2.0.3 + dev: false + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -4465,6 +4498,11 @@ packages: resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} dev: false + /highlight.js@11.9.0: + resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==} + engines: {node: '>=12.0.0'} + dev: false + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: false @@ -5154,6 +5192,14 @@ packages: js-tokens: 4.0.0 dev: false + /lowlight@3.1.0: + resolution: {integrity: sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==} + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.9.0 + dev: false + /lru-cache@10.2.0: resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} engines: {node: 14 || >=16.14}