a tool for shared writing and social publishing
1import {
2 BundledLanguage,
3 bundledLanguagesInfo,
4 bundledThemesInfo,
5 codeToHtml,
6} from "shiki";
7import { useEntity, useReplicache } from "src/replicache";
8import "katex/dist/katex.min.css";
9import { BlockLayout, BlockProps } from "./Block";
10import { useCallback, useLayoutEffect, useMemo, useState } from "react";
11import { useUIState } from "src/useUIState";
12import { BaseTextareaBlock } from "./BaseTextareaBlock";
13import { useEntitySetContext } from "components/EntitySetProvider";
14import { flushSync } from "react-dom";
15import { elementId } from "src/utils/elementId";
16import { LAST_USED_CODE_LANGUAGE_KEY } from "src/utils/codeLanguageStorage";
17import { focusBlock } from "src/utils/focusBlock";
18
19export function CodeBlock(props: BlockProps) {
20 let { rep, rootEntity } = useReplicache();
21 let content = useEntity(props.entityID, "block/code");
22 let lang =
23 useEntity(props.entityID, "block/code-language")?.data.value || "plaintext";
24
25 let theme =
26 useEntity(rootEntity, "theme/code-theme")?.data.value || "github-light";
27 let focusedBlock = useUIState(
28 (s) => s.focusedEntity?.entityID === props.entityID,
29 );
30 let entity_set = useEntitySetContext();
31 let { permissions } = entity_set;
32 const [html, setHTML] = useState<string | null>(null);
33
34 useLayoutEffect(() => {
35 if (!content) return;
36 void codeToHtml(content.data.value, {
37 lang,
38 theme,
39 structure: "classic",
40 }).then((h) => {
41 setHTML(h.replaceAll("<br>", "\n"));
42 });
43 }, [content, lang, theme]);
44
45 const onClick = useCallback((e: React.MouseEvent<HTMLElement>) => {
46 focusBlock(
47 { parent: props.parent, value: props.value, type: "code" },
48 { type: "end" },
49 );
50 }, []);
51 return (
52 <div className="codeBlock w-full flex flex-col rounded-md gap-0.5 ">
53 <BlockLayout
54 isSelected={focusedBlock}
55 hasBackground="accent"
56 borderOnHover
57 className="p-0! min-h-10 sm:min-h-12"
58 >
59 {focusedBlock && permissions.write ? (
60 <BaseTextareaBlock
61 placeholder="write some code…"
62 data-editable-block
63 data-entityid={props.entityID}
64 id={elementId.block(props.entityID).input}
65 block={props}
66 rep={rep}
67 permissionSet={entity_set.set}
68 spellCheck={false}
69 autoCapitalize="none"
70 autoCorrect="off"
71 className="codeBlockEditor whitespace-nowrap! overflow-auto! font-mono p-2 sm:p-3"
72 value={content?.data.value}
73 onChange={async (e) => {
74 // Update the entity with the new value
75 await rep?.mutate.assertFact({
76 attribute: "block/code",
77 entity: props.entityID,
78 data: { type: "string", value: e.target.value },
79 });
80 }}
81 />
82 ) : !html ? (
83 <pre
84 onClick={onClick}
85 onMouseDown={(e) => e.stopPropagation()}
86 className="codeBlockRendered overflow-auto! font-mono p-2 sm:p-3 w-full h-full"
87 >
88 {content?.data.value === "" || content?.data.value === undefined ? (
89 <div className="text-tertiary italic">write some code…</div>
90 ) : (
91 content?.data.value
92 )}
93 </pre>
94 ) : (
95 <div
96 onMouseDown={(e) => e.stopPropagation()}
97 onClick={onClick}
98 data-lang={lang}
99 className="contents"
100 dangerouslySetInnerHTML={{ __html: html || "" }}
101 />
102 )}
103 </BlockLayout>
104 {permissions.write && (
105 <div className="text-sm text-tertiary flex w-full justify-between">
106 <div className="codeBlockTheme grow flex gap-1">
107 Theme:{" "}
108 <select
109 className="codeBlockThemeSelect text-left bg-transparent pr-1 sm:max-w-none max-w-24 w-full"
110 onClick={(e) => {
111 e.preventDefault();
112 e.stopPropagation();
113 }}
114 value={theme}
115 onChange={async (e) => {
116 await rep?.mutate.assertFact({
117 attribute: "theme/code-theme",
118 entity: rootEntity,
119 data: { type: "string", value: e.target.value },
120 });
121 }}
122 >
123 {bundledThemesInfo.map((t) => (
124 <option key={t.id} value={t.id}>
125 {t.displayName}
126 </option>
127 ))}
128 </select>
129 </div>
130 <select
131 className="codeBlockLang grow text-right bg-transparent pr-1 sm:max-w-none max-w-24 w-full"
132 onClick={(e) => {
133 e.preventDefault();
134 e.stopPropagation();
135 }}
136 value={lang}
137 onChange={async (e) => {
138 localStorage.setItem(LAST_USED_CODE_LANGUAGE_KEY, e.target.value);
139 await rep?.mutate.assertFact({
140 attribute: "block/code-language",
141 entity: props.entityID,
142 data: { type: "string", value: e.target.value },
143 });
144 }}
145 >
146 <option value="plaintext">Plaintext</option>
147 {bundledLanguagesInfo.map((l) => (
148 <option key={l.id} value={l.id}>
149 {l.name}
150 </option>
151 ))}
152 </select>
153 </div>
154 )}
155 </div>
156 );
157}