diff --git a/package.json b/package.json
index 55087c2..96db5de 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"@popperjs/core": "2.11.x",
"@tailwindcss/forms": "0.5.x",
"@tailwindcss/typography": "0.5.x",
- "@types/highlight.js": "^9.12.4",
+ "@types/highlight.js": "^11.11.1",
"asciidoctor": "3.0.x",
"d3": "^7.9.0",
"he": "1.2.x",
diff --git a/src/app.css b/src/app.css
index ba7dfdb..a87e1a7 100644
--- a/src/app.css
+++ b/src/app.css
@@ -2,40 +2,38 @@
@import './styles/publications.css';
@import './styles/visualize.css';
-@layer components {
- /* General */
+/* Custom styles */
+@layer base {
.leather {
- @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300;
+ @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-200;
}
.btn-leather.text-xs {
- @apply w-7 h-7;
+ @apply px-2 py-1;
}
.btn-leather.text-xs svg {
- @apply w-3 h-3;
+ @apply h-3 w-3;
}
.btn-leather.text-sm {
- @apply w-8 h-8;
+ @apply px-3 py-2;
}
.btn-leather.text-sm svg {
- @apply w-4 h-4;
+ @apply h-4 w-4;
}
div[role='tooltip'] button.btn-leather {
@apply hover:text-primary-400 dark:hover:text-primary-500 hover:border-primary-400 dark:hover:border-primary-500 hover:bg-gray-200 dark:hover:bg-gray-700;
}
- /* Images */
.image-border {
@apply border border-primary-700;
}
- /* Card */
div.card-leather {
- @apply shadow-none text-primary-1000 border-s-4 bg-highlight border-primary-200 has-[:hover]:border-primary-700;
+ @apply shadow-none text-primary-1000 border-s-4 bg-highlight border-primary-200 has-[:hover]:border-primary-700;
@apply dark:bg-primary-1000 dark:border-primary-800 dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500;
}
@@ -52,7 +50,6 @@
@apply text-gray-900 hover:text-primary-600 dark:text-gray-200 dark:hover:text-primary-200;
}
- /* Content */
main {
@apply max-w-full;
}
@@ -74,7 +71,6 @@
@apply hover:bg-primary-100 dark:hover:bg-primary-800;
}
- /* Section headers */
h1.h-leather,
h2.h-leather,
h3.h-leather,
@@ -108,7 +104,6 @@
@apply text-base font-semibold;
}
- /* Modal */
div.modal-leather > div {
@apply bg-primary-0 dark:bg-primary-950 border-b-[1px] border-primary-100 dark:border-primary-600;
}
@@ -126,7 +121,6 @@
@apply bg-primary-0 hover:bg-primary-0 dark:bg-primary-950 dark:hover:bg-primary-950 text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500;
}
- /* Navbar */
nav.navbar-leather {
@apply bg-primary-0 dark:bg-primary-1000 z-10;
}
@@ -144,23 +138,20 @@
@apply text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500;
}
- /* Sidebar */
aside.sidebar-leather > div {
- @apply bg-gray-100 dark:bg-gray-900;
+ @apply bg-primary-0 dark:bg-primary-1000;
}
a.sidebar-item-leather {
@apply hover:bg-primary-100 dark:hover:bg-primary-800;
}
- /* Skeleton */
div.skeleton-leather div {
- @apply bg-gray-400 dark:bg-gray-600;
+ @apply bg-primary-100 dark:bg-primary-800;
}
- /* Textarea */
div.textarea-leather {
- @apply bg-gray-200 dark:bg-gray-800 border-gray-400 dark:border-gray-600;
+ @apply bg-primary-0 dark:bg-primary-1000;
}
div.textarea-leather > div:nth-child(1),
@@ -169,7 +160,7 @@
}
div.textarea-leather > div:nth-child(2) {
- @apply bg-gray-100 dark:bg-gray-900;
+ @apply bg-primary-0 dark:bg-primary-1000;
}
div.textarea-leather,
@@ -177,60 +168,66 @@
@apply text-gray-800 dark:text-gray-300;
}
- /* Tooltip */
div.tooltip-leather {
@apply text-gray-800 dark:text-gray-300;
}
div[role='tooltip'] button.btn-leather .tooltip-leather {
- @apply bg-gray-200 dark:bg-gray-700;
+ @apply bg-primary-100 dark:bg-primary-800;
}
-
- /* Unordered list */
+
.ul-leather li a {
@apply text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500;
}
+
.network-link-leather {
- @apply stroke-gray-400 fill-gray-400;
- }
- .network-node-leather {
- @apply stroke-gray-800;
- }
- .network-node-content {
- @apply fill-[#d6c1a8];
+ @apply stroke-primary-200 fill-primary-200;
}
- /* Code blocks */
- .code-block {
- @apply relative w-full max-w-[95%] overflow-x-auto rounded-lg bg-gray-100 dark:bg-gray-800 p-4 my-4 font-mono text-sm whitespace-pre;
- scrollbar-width: thin;
- scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
+ .network-node-leather {
+ @apply stroke-primary-600;
}
- .code-block::-webkit-scrollbar {
- height: 8px;
+ .network-node-content {
+ @apply fill-primary-100;
}
- .code-block::-webkit-scrollbar-track {
- @apply bg-transparent rounded-b-lg;
+ /* Code block styling - using highlight.js github-dark theme only */
+ pre code.hljs {
+ display: block;
+ overflow-x: auto;
+ padding: 1em;
}
- .code-block::-webkit-scrollbar-thumb {
- @apply bg-gray-400 dark:bg-gray-600 rounded-full;
+ .code-block {
+ @apply font-mono text-sm rounded-lg p-4 my-4 overflow-x-auto;
}
/* Inline code */
.inline-code {
- @apply font-mono text-sm bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded;
+ @apply font-mono text-sm rounded px-1.5 py-0.5;
+ @apply bg-primary-900 text-gray-200;
}
-}
-@layer components {
.leather-legend {
@apply flex-shrink-0 p-4 bg-primary-0 dark:bg-primary-1000 rounded-lg shadow
border border-gray-200 dark:border-gray-800;
}
+
.tooltip-leather {
- @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300;
+ @apply bg-gray-100 dark:bg-gray-900;
+ }
+
+ /* Adjusting text styles for better contrast */
+ em, i {
+ @apply text-gray-700 dark:text-gray-200; /* Darker in light mode, lighter in dark mode */
+ }
+
+ strong, b {
+ @apply text-gray-900 dark:text-gray-100; /* Darker in light mode, lighter in dark mode */
+ }
+
+ code {
+ @apply text-gray-800 dark:text-gray-200; /* Adjusted for better contrast */
}
}
diff --git a/src/lib/utils/advancedMarkdownParser.ts b/src/lib/utils/advancedMarkdownParser.ts
new file mode 100644
index 0000000..6ddfe56
--- /dev/null
+++ b/src/lib/utils/advancedMarkdownParser.ts
@@ -0,0 +1,416 @@
+import { parseBasicMarkdown } from './basicMarkdownParser';
+import hljs from 'highlight.js';
+import 'highlight.js/lib/common'; // Import common languages
+import 'highlight.js/styles/github-dark.css'; // Dark theme only
+import { processNostrIdentifiers } from './nostrUtils';
+
+// Register common languages
+hljs.configure({
+ ignoreUnescapedHTML: true
+});
+
+// Regular expressions for advanced markdown elements
+const HEADING_REGEX = /^(#{1,6})\s+(.+)$/gm;
+const ALTERNATE_HEADING_REGEX = /^([^\n]+)\n(=+|-+)\n/gm;
+const INLINE_CODE_REGEX = /`([^`\n]+)`/g;
+const LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g;
+const IMAGE_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g;
+const HORIZONTAL_RULE_REGEX = /^(?:[-*_]\s*){3,}$/gm;
+const FOOTNOTE_REFERENCE_REGEX = /\[\^([^\]]+)\]/g;
+const FOOTNOTE_DEFINITION_REGEX = /^\[\^([^\]]+)\]:\s*(.+)$/gm;
+
+interface Footnote {
+ id: string;
+ text: string;
+ referenceCount: number;
+}
+
+interface FootnoteReference {
+ id: string;
+ count: number;
+}
+
+/**
+ * Process headings (both styles)
+ */
+function processHeadings(content: string): string {
+ // Process ATX-style headings (# Heading)
+ let processedContent = content.replace(HEADING_REGEX, (_, level, text) => {
+ const headingLevel = level.length;
+ return `
| ${cell} | \n`; + }); + html += '
|---|
| ${cell} | \n`; + }); + html += '
${text}`; + }); +} + +/** + * Process code blocks by finding consecutive code lines and preserving their content + */ +function processCodeBlocks(text: string): { text: string; blocks: Map
${highlighted}`;
+ } catch (e) {
+ console.warn('Failed to highlight code block:', e);
+ html = `${code}`;
+ }
+ } else {
+ html = `${code}`;
+ }
+
+ result = result.replace(id, html);
+ } catch (error) {
+ console.error('Error restoring code block:', error);
+ result = result.replace(id, 'Error processing code block');
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Parse markdown text with advanced formatting
+ */
+export async function parseAdvancedMarkdown(text: string): Promise${escapedCode}`;
+ });
+
+ // Process footnotes before basic markdown to prevent unwanted paragraph tags
+ processedText = processFootnotes(processedText);
+
+ // Process async elements
+ processedText = await processNostrIdentifiers(processedText);
+ processedText = await parseBasicMarkdown(processedText);
+
+ // Step 3: Restore code blocks
+ processedText = restoreCodeBlocks(processedText, blocks);
+
+ return processedText;
+ } catch (error) {
+ console.error('Error in parseAdvancedMarkdown:', error);
+ if (error instanceof Error) {
+ return `${lines.join('\n')}
$1');
+
+ return content;
+ } catch (error) {
+ console.error('Error in processBasicFormatting:', error);
+ return content;
+ }
+}
+
+/**
+ * Process blockquotes
+ */
+function processBlockquotes(content: string): string {
+ try {
+ if (!content) return '';
+
+ return content.replace(BLOCKQUOTE_REGEX, match => {
+ // Split into lines and process each line
+ const lines = match.split('\n').map(line => {
+ // Remove the '>' marker and trim any whitespace after it
+ return line.replace(/^[ \t]*>[ \t]?/, '').trim();
+ });
+
+ // Join the lines with proper spacing and wrap in blockquote
+ return `${ + lines.join('\n') + }`; + }); + } catch (error) { + console.error('Error in processBlockquotes:', error); + return content; + } +} + +/** + * Calculate indentation level from spaces + */ +function getIndentLevel(spaces: string): number { + return Math.floor(spaces.length / 2); +} + +/** + * Process lists (ordered and unordered) + */ +function processLists(content: string): string { + const lines = content.split('\n'); + const processed: string[] = []; + const listStack: { type: 'ol' | 'ul', items: string[], level: number }[] = []; + + function closeList() { + if (listStack.length > 0) { + const list = listStack.pop()!; + const listType = list.type; + const listClass = listType === 'ol' ? 'list-decimal' : 'list-disc'; + const indentClass = list.level > 0 ? 'ml-6' : 'ml-4'; + let listHtml = `<${listType} class="${listClass} ${indentClass} my-2 space-y-2">`; + list.items.forEach(item => { + listHtml += `\n
${code}`;
+ });
+}
// Regular expressions for markdown elements
const BOLD_REGEX = /\*\*([^*]+)\*\*|\*([^*]+)\*/g;
@@ -28,614 +23,4 @@ const TABLE_REGEX = /^\|(.+)\|\r?\n\|([-|\s]+)\|\r?\n((?:\|.+\|\r?\n?)+)$/gm;
const TABLE_ROW_REGEX = /^\|(.+)\|$/gm;
const TABLE_DELIMITER_REGEX = /^[\s-]+$/;
-// Cache for npub metadata
-const npubCache = new Map${quoteContent}
${quoteContent}
${quoteContent}
${processedCode}
- ${code}`;
- });
-}
-
-/**
- * Process markdown tables
- */
-function processTables(content: string): string {
- return content.replace(TABLE_REGEX, (match, headerRow, delimiterRow, bodyRows) => {
- // Process header row
- const headers: string[] = headerRow
- .split('|')
- .map((cell: string) => cell.trim())
- .filter((cell: string) => cell.length > 0);
-
- // Validate delimiter row (should contain only dashes and spaces)
- const delimiters: string[] = delimiterRow
- .split('|')
- .map((cell: string) => cell.trim())
- .filter((cell: string) => cell.length > 0);
-
- if (!delimiters.every(d => TABLE_DELIMITER_REGEX.test(d))) {
- return match;
- }
-
- // Process body rows
- const rows: string[][] = bodyRows
- .trim()
- .split('\n')
- .map((row: string) => {
- return row
- .split('|')
- .map((cell: string) => cell.trim())
- .filter((cell: string) => cell.length > 0);
- })
- .filter((row: string[]) => row.length > 0);
-
- // Generate HTML table with leather theme styling and thicker grid lines
- let table = '| ${header} | \n`; - }); - table += '
|---|
| ${cell} | \n`; - }); - table += '
${para}
`) - .join('\n\n'); - - // Finally, restore code blocks - content = restoreCodeBlocks(content, blocks); - - return content; -} - -/** - * Escape special characters in a string for use in a regular expression - */ -function escapeRegExp(string: string): string { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -function processCode(text: string): string { - // Process code blocks with language specification - text = text.replace(/```(\w+)?\n([\s\S]+?)\n```/g, (match, lang, code) => { - if (lang === 'json') { - try { - const parsed = JSON.parse(code.trim()); - code = JSON.stringify(parsed, null, 2); - // Add syntax highlighting classes for JSON - code = code.replace(/"([^"]+)":/g, '"$1":') // keys - .replace(/"([^"]+)"/g, '"$1"') // strings - .replace(/\b(true|false)\b/g, '$1') // booleans - .replace(/\b(null)\b/g, '$1') // null - .replace(/\b(\d+\.?\d*)\b/g, '$1'); // numbers - } catch (e) { - // If JSON parsing fails, use the original code - } - } - return `$1');
-
- return text;
-}
+// ... existing code ...
\ No newline at end of file
diff --git a/src/lib/utils/markdownTestfile.md b/src/lib/utils/markdownTestfile.md
index c73a4dd..cb35194 100644
--- a/src/lib/utils/markdownTestfile.md
+++ b/src/lib/utils/markdownTestfile.md
@@ -3,7 +3,9 @@ This is a test
### Disclaimer
-It is _only_ a test. I just wanted to see if the markdown renders correctly on the page, even if I use **two asterisks** for bold text, instead of *one asterisk*.[^1]
+It is _only_ a test, for __sure__. I just wanted to see if the markdown renders correctly on the page, even if I use **two asterisks** for bold text, instead of *one asterisk*.[^1]
+
+This file is full of ~errors~ opportunities to ~~mess up the formatting~~ check your markdown parser.
npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z wrote this. That's the same person as nostr:npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z and nprofile1qydhwumn8ghj7argv4nx7un9wd6zumn0wd68yvfwvdhk6tcpr3mhxue69uhkx6rjd9ehgurfd3kzumn0wd68yvfwvdhk6tcqyr7jprhgeregx7q2j4fgjmjgy0xfm34l63pqvwyf2acsd9q0mynuzp4qva3. That is a different person from npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz.
@@ -40,6 +42,9 @@ Let's nest that:
3. third
4. fourth indented
5. fifth indented even more
+ 6. sixth under the fourth
+ 7. seventh under the sixth
+8. eighth under the third
This is ordered and unordered mixed:
1. first
@@ -67,7 +72,7 @@ nostr:naddr1qvzqqqr4gupzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqydh
This is an implementation of [Nostr-flavored Markdown](https://github.com/nostrability/nostrability/issues/146) for #gitstuff issue notes.
-You can even include `code inline` or
+You can even include `code inline`, like `