diff --git a/packages/app/package.json b/packages/app/package.json
index 2ebc9af2..add96928 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -19,6 +19,7 @@
"debug": "^4.3.4",
"dexie": "^3.2.4",
"emojilib": "^3.0.10",
+ "highlight.js": "^11.8.0",
"light-bolt11-decoder": "^2.1.0",
"match-sorter": "^6.3.1",
"qr-code-styling": "^1.6.0-rc.1",
diff --git a/packages/app/src/Element/CodeBlock.css b/packages/app/src/Element/CodeBlock.css
new file mode 100644
index 00000000..af8a21e8
--- /dev/null
+++ b/packages/app/src/Element/CodeBlock.css
@@ -0,0 +1,14 @@
+.codeblock {
+ overflow: auto;
+ position: relative;
+}
+
+.codeblock pre {
+ overflow: auto;
+ line-height: 1.4;
+ font-size: var(--font-size);
+}
+
+.hljs {
+ background: #f6f8fa;
+}
diff --git a/packages/app/src/Element/CodeBlock.tsx b/packages/app/src/Element/CodeBlock.tsx
new file mode 100644
index 00000000..825c1ccb
--- /dev/null
+++ b/packages/app/src/Element/CodeBlock.tsx
@@ -0,0 +1,24 @@
+import { useEffect } from "react";
+import "highlight.js/styles/github.css";
+import "./CodeBlock.css";
+
+const CodeBlock = ({ content, language }: { content: string; language?: string }) => {
+ useEffect(() => {
+ const importHljs = async () => {
+ const hljs = (await import("highlight.js")).default;
+ hljs.highlightAll();
+ };
+
+ importHljs();
+ });
+
+ return (
+
+ );
+};
+
+export default CodeBlock;
diff --git a/packages/app/src/Element/Text.tsx b/packages/app/src/Element/Text.tsx
index 31fea6dc..06f00ffd 100644
--- a/packages/app/src/Element/Text.tsx
+++ b/packages/app/src/Element/Text.tsx
@@ -11,6 +11,7 @@ import { ProxyImg } from "./ProxyImg";
import { SpotlightMediaModal } from "./Deck/SpotlightMedia";
import HighlightedText from "./HighlightedText";
import { useTextTransformer } from "Hooks/useTextTransformCache";
+import CodeBlock from "./CodeBlock";
export interface TextProps {
id: string;
@@ -254,6 +255,9 @@ export default function Text({
if (element.type === "custom_emoji") {
chunks.push();
}
+ if (element.type === "code_block") {
+ chunks.push();
+ }
if (element.type === "text") {
chunks.push(
diff --git a/packages/system/src/const.ts b/packages/system/src/const.ts
index af06cced..28159825 100644
--- a/packages/system/src/const.ts
+++ b/packages/system/src/const.ts
@@ -34,3 +34,8 @@ export const CashuRegex = /(cashuA[A-Za-z0-9_-]{0,10000}={0,3})/i;
* Regex to match any npub/nevent/naddr/nprofile/note
*/
export const MentionNostrEntityRegex = /@n(pub|profile|event|ote|addr|)1[acdefghjklmnpqrstuvwxyz023456789]+/g;
+
+/**
+ * Regex to match markdown code content
+ */
+export const MarkdownCodeRegex = /(```.+?```)/gms;
diff --git a/packages/system/src/text.ts b/packages/system/src/text.ts
index 89b9514f..5988b40a 100644
--- a/packages/system/src/text.ts
+++ b/packages/system/src/text.ts
@@ -1,13 +1,31 @@
import { unwrap } from "@snort/shared";
-import { CashuRegex, FileExtensionRegex, HashtagRegex, InvoiceRegex, MentionNostrEntityRegex } from "./const";
+import {
+ CashuRegex,
+ FileExtensionRegex,
+ HashtagRegex,
+ InvoiceRegex,
+ MarkdownCodeRegex,
+ MentionNostrEntityRegex,
+} from "./const";
import { validateNostrLink } from "./nostr-link";
import { splitByUrl } from "./utils";
export interface ParsedFragment {
- type: "text" | "link" | "mention" | "invoice" | "media" | "cashu" | "hashtag" | "custom_emoji" | "highlighted_text";
+ type:
+ | "text"
+ | "link"
+ | "mention"
+ | "invoice"
+ | "media"
+ | "cashu"
+ | "hashtag"
+ | "custom_emoji"
+ | "highlighted_text"
+ | "code_block";
content: string;
mimeType?: string;
+ language?: string;
}
export type Fragment = string | ParsedFragment;
@@ -179,6 +197,31 @@ function extractCustomEmoji(fragments: Fragment[], tags: Array
>) {
.flat();
}
+function extractMarkdownCode(fragments: Fragment[]): (string | ParsedFragment)[] {
+ return fragments
+ .map(f => {
+ if (typeof f === "string") {
+ return f.split(MarkdownCodeRegex).map(i => {
+ if (i.startsWith("```") && i.endsWith("```")) {
+ const firstLineBreakIndex = i.indexOf("\n");
+ const lastLineBreakIndex = i.lastIndexOf("\n");
+
+ return {
+ type: "code_block",
+ content: i.substring(firstLineBreakIndex, lastLineBreakIndex),
+ language: i.substring(3, firstLineBreakIndex),
+ } as ParsedFragment;
+ } else {
+ return i;
+ }
+ });
+ }
+
+ return f;
+ })
+ .flat();
+}
+
export function transformText(body: string, tags: Array>) {
let fragments = extractLinks([body]);
fragments = extractMentions(fragments);
@@ -186,6 +229,7 @@ export function transformText(body: string, tags: Array>) {
fragments = extractInvoices(fragments);
fragments = extractCashuTokens(fragments);
fragments = extractCustomEmoji(fragments, tags);
+ fragments = extractMarkdownCode(fragments);
fragments = fragments
.map(a => {
if (typeof a === "string") {
diff --git a/yarn.lock b/yarn.lock
index 6ecf2593..2c26368b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2718,6 +2718,7 @@ __metadata:
emojilib: ^3.0.10
eslint: ^8.48.0
eslint-webpack-plugin: ^4.0.1
+ highlight.js: ^11.8.0
html-webpack-plugin: ^5.5.1
jest: ^29.5.0
jest-environment-jsdom: ^29.5.0
@@ -7278,6 +7279,13 @@ __metadata:
languageName: node
linkType: hard
+"highlight.js@npm:^11.8.0":
+ version: 11.8.0
+ resolution: "highlight.js@npm:11.8.0"
+ checksum: d2578a57aee7315946ff19379053fd0a28b127baabf7617ab1d28d62cdc4eaf3d75053569cb8479a5afdc7a68f1ba9a6c1d612d8ae399b4b9aa43093b4fb6831
+ languageName: node
+ linkType: hard
+
"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"