Browse Source

refactor: Remove legacy parsing code and clean up AST integration

Complete the AST-based parsing migration by removing all deprecated manual
parsing functions. The codebase now exclusively uses Asciidoctor's AST
for document processing.

- Remove parseAsciiDocWithMetadata, parseAsciiDocIterative, generateNostrEvents
- Delete 484 lines of legacy parsing code from asciidoc_metadata.ts
- Clean up imports and formatting across affected files
- Fix import paths to use consistent module resolution
- Remove unused UI imports from compose page

This completes the transition to AST-based PublicationTree architecture.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
master
limina1 7 months ago
parent
commit
0afd2e2ba4
  1. 4
      src/lib/components/ZettelEditor.svelte
  2. 4
      src/lib/services/publisher.ts
  3. 484
      src/lib/utils/asciidoc_metadata.ts
  4. 4
      src/lib/utils/event_input_utils.ts
  5. 188
      src/lib/utils/publication_tree_factory.ts
  6. 59
      src/routes/new/compose/+page.svelte
  7. 4
      tests/unit/metadataExtraction.test.ts

4
src/lib/components/ZettelEditor.svelte

@ -3,10 +3,6 @@ @@ -3,10 +3,6 @@
import { EyeOutline, QuestionCircleOutline } from "flowbite-svelte-icons";
import {
extractSmartMetadata,
parseAsciiDocWithMetadata,
parseAsciiDocIterative,
generateNostrEvents,
detectContentType,
type AsciiDocMetadata,
metadataToTags,
parseSimpleAttributes,

4
src/lib/services/publisher.ts

@ -1,8 +1,10 @@ @@ -1,8 +1,10 @@
import { getMimeTags } from "../utils/mime.ts";
import {
metadataToTags,
parseAsciiDocWithMetadata,
} from "../utils/asciidoc_metadata.ts";
import {
parseAsciiDocWithMetadata,
} from "../utils/asciidoc_parser.ts";
import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools";

484
src/lib/utils/asciidoc_metadata.ts

@ -29,16 +29,6 @@ export interface AsciiDocMetadata { @@ -29,16 +29,6 @@ export interface AsciiDocMetadata {
export type SectionMetadata = AsciiDocMetadata;
export interface ParsedAsciiDoc {
metadata: AsciiDocMetadata;
content: string;
title: string;
sections: Array<{
metadata: SectionMetadata;
content: string;
title: string;
}>;
}
// Shared attribute mapping based on Asciidoctor standard attributes
const ATTRIBUTE_MAP: Record<string, keyof AsciiDocMetadata> = {
@ -554,53 +544,6 @@ export function extractSectionMetadata(inputSectionContent: string): { @@ -554,53 +544,6 @@ export function extractSectionMetadata(inputSectionContent: string): {
return { metadata, content, title };
}
/**
* Parses AsciiDoc content into sections with metadata
*/
export function parseAsciiDocWithMetadata(content: string): ParsedAsciiDoc {
const asciidoctor = createProcessor();
const document = asciidoctor.load(content, { standalone: false }) as Document;
const { metadata: docMetadata } = extractDocumentMetadata(content);
// Parse the original content to find section attributes
const lines = content.split(/\r?\n/);
const sectionsWithMetadata: Array<{
metadata: SectionMetadata;
content: string;
title: string;
}> = [];
let currentSection: string | null = null;
let currentSectionContent: string[] = [];
for (const line of lines) {
if (line.match(/^==\s+/)) {
// Save previous section if exists
if (currentSection) {
const sectionContent = currentSectionContent.join("\n");
sectionsWithMetadata.push(extractSectionMetadata(sectionContent));
}
// Start new section
currentSection = line;
currentSectionContent = [line];
} else if (currentSection) {
currentSectionContent.push(line);
}
}
// Save the last section
if (currentSection) {
const sectionContent = currentSectionContent.join("\n");
sectionsWithMetadata.push(extractSectionMetadata(sectionContent));
}
return {
metadata: docMetadata,
content: document.getSource(),
title: docMetadata.title || '',
sections: sectionsWithMetadata
};
}
/**
* Converts metadata to Nostr event tags
@ -701,437 +644,10 @@ export function extractMetadataFromSectionsOnly(content: string): { @@ -701,437 +644,10 @@ export function extractMetadataFromSectionsOnly(content: string): {
return { metadata, content };
}
/**
* Iterative AsciiDoc parsing based on specified level
* Level 2: Only == sections become content events (containing all subsections)
* Level 3: == sections become indices + content events, === sections become content events
* Level 4: === sections become indices + content events, ==== sections become content events, etc.
*/
export function parseAsciiDocIterative(content: string, parseLevel: number = 2): ParsedAsciiDoc {
const asciidoctor = createProcessor();
const document = asciidoctor.load(content, { standalone: false }) as Document;
const { metadata: docMetadata } = extractDocumentMetadata(content);
const lines = content.split(/\r?\n/);
const sections: Array<{
metadata: SectionMetadata;
content: string;
title: string;
}> = [];
if (parseLevel === 2) {
// Level 2: Only == sections become events
const level2Pattern = /^==\s+/;
let currentSection: string | null = null;
let currentSectionContent: string[] = [];
let documentContent: string[] = [];
let inDocumentHeader = true;
for (const line of lines) {
if (line.match(level2Pattern)) {
inDocumentHeader = false;
// Save previous section if exists
if (currentSection) {
const sectionContent = currentSectionContent.join('\n');
const sectionMeta = extractSectionMetadata(sectionContent);
// For level 2, preserve the full content including the header
sections.push({
...sectionMeta,
content: sectionContent // Use full content, not stripped
});
}
// Start new section
currentSection = line;
currentSectionContent = [line];
} else if (currentSection) {
currentSectionContent.push(line);
} else if (inDocumentHeader) {
documentContent.push(line);
}
}
// Save the last section
if (currentSection) {
const sectionContent = currentSectionContent.join('\n');
const sectionMeta = extractSectionMetadata(sectionContent);
// For level 2, preserve the full content including the header
sections.push({
...sectionMeta,
content: sectionContent // Use full content, not stripped
});
}
const docContent = documentContent.join('\n');
return {
metadata: docMetadata,
content: docContent,
title: docMetadata.title || '',
sections: sections
};
}
// Level 3+: Parse hierarchically
// All levels from 2 to parseLevel-1 are indices (title only)
// Level parseLevel are content sections (full content)
// First, collect all sections at the content level (parseLevel)
const contentLevelPattern = new RegExp(`^${'='.repeat(parseLevel)}\\s+`);
let currentSection: string | null = null;
let currentSectionContent: string[] = [];
let documentContent: string[] = [];
let inDocumentHeader = true;
for (const line of lines) {
if (line.match(contentLevelPattern)) {
inDocumentHeader = false;
// Save previous section if exists
if (currentSection) {
const sectionContent = currentSectionContent.join('\n');
const sectionMeta = extractSectionMetadata(sectionContent);
sections.push({
...sectionMeta,
content: sectionContent // Full content including headers
});
}
// Start new content section
currentSection = line;
currentSectionContent = [line];
} else if (currentSection) {
// Continue collecting content for current section
currentSectionContent.push(line);
} else if (inDocumentHeader) {
documentContent.push(line);
}
}
// Save the last section
if (currentSection) {
const sectionContent = currentSectionContent.join('\n');
const sectionMeta = extractSectionMetadata(sectionContent);
sections.push({
...sectionMeta,
content: sectionContent // Full content including headers
});
}
// Now collect index sections (all levels from 2 to parseLevel-1)
// These should be shown as navigation/structure but not full content
const indexSections: Array<{
metadata: SectionMetadata;
content: string;
title: string;
level: number;
}> = [];
for (let level = 2; level < parseLevel; level++) {
const levelPattern = new RegExp(`^${'='.repeat(level)}\\s+(.+)$`, 'gm');
const matches = content.matchAll(levelPattern);
for (const match of matches) {
const title = match[1].trim();
indexSections.push({
metadata: { title },
content: `${'='.repeat(level)} ${title}`, // Just the header line for index sections
title,
level
});
}
}
// Add actual level to content sections based on their content
const contentSectionsWithLevel = sections.map(s => ({
...s,
level: getSectionLevel(s.content)
}));
// Combine index sections and content sections
// Sort by position in original content to maintain order
const allSections = [...indexSections, ...contentSectionsWithLevel];
// Sort sections by their appearance in the original content
allSections.sort((a, b) => {
const posA = content.indexOf(a.content.split('\n')[0]);
const posB = content.indexOf(b.content.split('\n')[0]);
return posA - posB;
});
const docContent = documentContent.join('\n');
return {
metadata: docMetadata,
content: docContent,
title: docMetadata.title || '',
sections: allSections
};
}
/**
* Helper function to determine the header level of a section
*/
function getSectionLevel(sectionContent: string): number {
const lines = sectionContent.split(/\r?\n/);
for (const line of lines) {
const match = line.match(/^(=+)\s+/);
if (match) {
return match[1].length;
}
}
return 0;
}
/**
* Helper function to extract just the intro content (before first subsection)
*/
function extractIntroContent(sectionContent: string, currentLevel: number): string {
const lines = sectionContent.split(/\r?\n/);
const introLines: string[] = [];
let foundHeader = false;
for (const line of lines) {
const headerMatch = line.match(/^(=+)\s+/);
if (headerMatch) {
const level = headerMatch[1].length;
if (level === currentLevel && !foundHeader) {
// This is the section header itself
foundHeader = true;
continue; // Skip the header line itself for intro content
} else if (level > currentLevel) {
// This is a subsection, stop collecting intro content
break;
}
} else if (foundHeader) {
// This is intro content after the header
introLines.push(line);
}
}
return introLines.join('\n').trim();
}
/**
* Generates Nostr events from parsed AsciiDoc with proper hierarchical structure
* Based on docreference.md specifications
*/
export function generateNostrEvents(parsed: ParsedAsciiDoc, parseLevel: number = 2, pubkey?: string, maxDepth: number = 6): {
indexEvent?: any;
contentEvents: any[];
} {
const allEvents: any[] = [];
const actualPubkey = pubkey || 'pubkey';
// Helper function to generate section ID
const generateSectionId = (title: string): string => {
return title
.toLowerCase()
.replace(/[^\p{L}\p{N}]/gu, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
};
// Build hierarchical tree structure
interface TreeNode {
section: {
metadata: any;
content: string;
title: string;
};
level: number;
sectionId: string;
tags: [string, string][];
children: TreeNode[];
parent?: TreeNode;
}
// Convert flat sections to tree structure
const buildTree = (): TreeNode[] => {
const roots: TreeNode[] = [];
const stack: TreeNode[] = [];
for (const section of parsed.sections) {
const level = getSectionLevel(section.content);
const sectionId = generateSectionId(section.title);
const tags = parseSimpleAttributes(section.content);
const node: TreeNode = {
section,
level,
sectionId,
tags,
children: [],
};
// Find the correct parent based on header hierarchy
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
stack.pop();
}
if (stack.length === 0) {
// This is a root level section
roots.push(node);
} else {
// This is a child of the last item in stack
const parent = stack[stack.length - 1];
parent.children.push(node);
node.parent = parent;
}
stack.push(node);
}
return roots;
};
const tree = buildTree();
// Recursively create events from tree
const createEventsFromNode = (node: TreeNode): void => {
const { section, level, sectionId, tags, children } = node;
// Determine if this node should become an index
const hasChildrenAtTargetLevel = children.some(child => child.level === parseLevel);
const shouldBeIndex = level < parseLevel && (hasChildrenAtTargetLevel || children.some(child => child.level <= parseLevel));
if (shouldBeIndex) {
// Create content event for intro text (30041)
const introContent = extractIntroContent(section.content, level);
if (introContent.trim()) {
const contentEvent = {
id: '',
pubkey: '',
created_at: Math.floor(Date.now() / 1000),
kind: 30041,
tags: [
['d', `${sectionId}-content`],
['title', section.title],
...tags
],
content: introContent,
sig: ''
};
allEvents.push(contentEvent);
}
// Create index event (30040)
const childATags: string[][] = [];
// Add a-tag for intro content if it exists
if (introContent.trim()) {
childATags.push(['a', `30041:${actualPubkey}:${sectionId}-content`, '', '']);
}
// Add a-tags for direct children
for (const child of children) {
const childHasSubChildren = child.children.some(grandchild => grandchild.level <= parseLevel);
const childShouldBeIndex = child.level < parseLevel && childHasSubChildren;
const childKind = childShouldBeIndex ? 30040 : 30041;
childATags.push(['a', `${childKind}:${actualPubkey}:${child.sectionId}`, '', '']);
}
const indexEvent = {
id: '',
pubkey: '',
created_at: Math.floor(Date.now() / 1000),
kind: 30040,
tags: [
['d', sectionId],
['title', section.title],
...tags,
...childATags
],
content: '',
sig: ''
};
allEvents.push(indexEvent);
} else {
// Create regular content event (30041)
const contentEvent = {
id: '',
pubkey: '',
created_at: Math.floor(Date.now() / 1000),
kind: 30041,
tags: [
['d', sectionId],
['title', section.title],
...tags
],
content: section.content,
sig: ''
};
allEvents.push(contentEvent);
}
// Recursively process children
for (const child of children) {
createEventsFromNode(child);
}
};
// Process all root level sections
for (const rootNode of tree) {
createEventsFromNode(rootNode);
}
// Create main document index if we have a document title (article format)
if (parsed.title && parsed.title.trim() !== '') {
const documentId = generateSectionId(parsed.title);
const documentTags = parseSimpleAttributes(parsed.content);
// Create a-tags for all root level sections (level 2)
const mainIndexATags = tree.map(rootNode => {
const hasSubChildren = rootNode.children.some(child => child.level <= parseLevel);
const shouldBeIndex = rootNode.level < parseLevel && hasSubChildren;
const kind = shouldBeIndex ? 30040 : 30041;
return ['a', `${kind}:${actualPubkey}:${rootNode.sectionId}`, '', ''];
});
console.log('Debug: Root sections found:', tree.length);
console.log('Debug: Main index a-tags:', mainIndexATags);
const mainIndexEvent = {
id: '',
pubkey: '',
created_at: Math.floor(Date.now() / 1000),
kind: 30040,
tags: [
['d', documentId],
['title', parsed.title],
...documentTags,
...mainIndexATags
],
content: '',
sig: ''
};
return {
indexEvent: mainIndexEvent,
contentEvents: allEvents
};
}
// For scattered notes, return only content events
return {
contentEvents: allEvents
};
}
/**
* Detects content type for smart publishing
*/
export function detectContentType(content: string): 'article' | 'scattered-notes' | 'none' {
const hasDocTitle = content.trim().startsWith('=') && !content.trim().startsWith('==');
const hasSections = content.includes('==');
if (hasDocTitle) {
return 'article';
} else if (hasSections) {
return 'scattered-notes';
} else {
return 'none';
}
}
/**
* Smart metadata extraction that handles both document headers and section-only content

4
src/lib/utils/event_input_utils.ts

@ -4,8 +4,10 @@ import { EVENT_KINDS } from "./search_constants"; @@ -4,8 +4,10 @@ import { EVENT_KINDS } from "./search_constants";
import {
extractDocumentMetadata,
metadataToTags,
parseAsciiDocWithMetadata,
} from "./asciidoc_metadata.ts";
import {
parseAsciiDocWithMetadata,
} from "./asciidoc_parser.ts";
// =========================
// Validation

188
src/lib/utils/publication_tree_factory.ts

@ -5,12 +5,12 @@ @@ -5,12 +5,12 @@
* providing a clean bridge between AsciiDoc parsing and Nostr event publishing.
*/
import { PublicationTree } from "../data_structures/publication_tree.ts";
import { SveltePublicationTree } from "../components/publications/svelte_publication_tree.svelte.ts";
import { parseAsciiDocAST } from "./asciidoc_ast_parser.ts";
import { PublicationTree } from "$lib/data_structures/publication_tree.ts";
import { SveltePublicationTree } from "$lib/components/publications/svelte_publication_tree.svelte.ts";
import { parseAsciiDocAST } from "asciidoc_ast_parser.ts";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import type NDK from "@nostr-dev-kit/ndk";
import { getMimeTags } from "./mime.ts";
import { getMimeTags } from "mime.ts";
export interface PublicationTreeFactoryResult {
tree: PublicationTree;
@ -20,7 +20,7 @@ export interface PublicationTreeFactoryResult { @@ -20,7 +20,7 @@ export interface PublicationTreeFactoryResult {
metadata: {
title: string;
totalSections: number;
contentType: 'article' | 'scattered-notes' | 'none';
contentType: "article" | "scattered-notes" | "none";
attributes: Record<string, string>;
};
}
@ -32,9 +32,8 @@ export interface PublicationTreeFactoryResult { @@ -32,9 +32,8 @@ export interface PublicationTreeFactoryResult {
export async function createPublicationTreeFromContent(
content: string,
ndk: NDK,
parseLevel: number = 2
parseLevel: number = 2,
): Promise<PublicationTreeFactoryResult> {
// For preview purposes, we can work without authentication
// Authentication is only required for actual publishing
const hasActiveUser = !!ndk.activeUser;
@ -49,7 +48,7 @@ export async function createPublicationTreeFromContent( @@ -49,7 +48,7 @@ export async function createPublicationTreeFromContent(
let indexEvent: NDKEvent | null = null;
const contentEvents: NDKEvent[] = [];
if (contentType === 'article' && parsed.title) {
if (contentType === "article" && parsed.title) {
// Create hierarchical structure: 30040 index + 30041 content events
indexEvent = createIndexEvent(parsed, ndk);
tree = new PublicationTree(indexEvent, ndk);
@ -60,8 +59,7 @@ export async function createPublicationTreeFromContent( @@ -60,8 +59,7 @@ export async function createPublicationTreeFromContent(
await tree.addEvent(contentEvent, indexEvent);
contentEvents.push(contentEvent);
}
} else if (contentType === 'scattered-notes') {
} else if (contentType === "scattered-notes") {
// Create flat structure: only 30041 events
if (parsed.sections.length === 0) {
throw new Error("No sections found for scattered notes");
@ -79,7 +77,6 @@ export async function createPublicationTreeFromContent( @@ -79,7 +77,6 @@ export async function createPublicationTreeFromContent(
await tree.addEvent(contentEvent, rootEvent);
contentEvents.push(contentEvent);
}
} else {
throw new Error("No valid content found to create publication tree");
}
@ -87,7 +84,7 @@ export async function createPublicationTreeFromContent( @@ -87,7 +84,7 @@ export async function createPublicationTreeFromContent(
// Create reactive Svelte wrapper
const svelteTree = new SveltePublicationTree(
indexEvent || contentEvents[0],
ndk
ndk,
);
return {
@ -99,8 +96,8 @@ export async function createPublicationTreeFromContent( @@ -99,8 +96,8 @@ export async function createPublicationTreeFromContent(
title: parsed.title,
totalSections: parsed.sections.length,
contentType,
attributes: parsed.attributes
}
attributes: parsed.attributes,
},
};
}
@ -112,18 +109,13 @@ function createIndexEvent(parsed: any, ndk: NDK): NDKEvent { @@ -112,18 +109,13 @@ function createIndexEvent(parsed: any, ndk: NDK): NDKEvent {
event.kind = 30040;
event.created_at = Math.floor(Date.now() / 1000);
// Use placeholder pubkey for preview if no active user
event.pubkey = ndk.activeUser?.pubkey || 'preview-placeholder-pubkey';
event.pubkey = ndk.activeUser?.pubkey || "preview-placeholder-pubkey";
// Generate d-tag from title
const dTag = generateDTag(parsed.title);
const [mTag, MTag] = getMimeTags(30040);
const tags: string[][] = [
["d", dTag],
mTag,
MTag,
["title", parsed.title]
];
const tags: string[][] = [["d", dTag], mTag, MTag, ["title", parsed.title]];
// Add document attributes as tags
addDocumentAttributesToTags(tags, parsed.attributes, event.pubkey);
@ -143,23 +135,22 @@ function createIndexEvent(parsed: any, ndk: NDK): NDKEvent { @@ -143,23 +135,22 @@ function createIndexEvent(parsed: any, ndk: NDK): NDKEvent {
/**
* Create a 30041 content event from parsed section
*/
function createContentEvent(section: any, documentParsed: any, ndk: NDK): NDKEvent {
function createContentEvent(
section: any,
documentParsed: any,
ndk: NDK,
): NDKEvent {
const event = new NDKEvent(ndk);
event.kind = 30041;
event.created_at = Math.floor(Date.now() / 1000);
// Use placeholder pubkey for preview if no active user
event.pubkey = ndk.activeUser?.pubkey || 'preview-placeholder-pubkey';
event.pubkey = ndk.activeUser?.pubkey || "preview-placeholder-pubkey";
const dTag = generateDTag(section.title);
const [mTag, MTag] = getMimeTags(30041);
const tags: string[][] = [
["d", dTag],
mTag,
MTag,
["title", section.title]
];
const tags: string[][] = [["d", dTag], mTag, MTag, ["title", section.title]];
// Add section-specific attributes
addSectionAttributesToTags(tags, section.attributes);
@ -168,7 +159,7 @@ function createContentEvent(section: any, documentParsed: any, ndk: NDK): NDKEve @@ -168,7 +159,7 @@ function createContentEvent(section: any, documentParsed: any, ndk: NDK): NDKEve
inheritDocumentAttributes(tags, documentParsed.attributes);
event.tags = tags;
event.content = section.content || '';
event.content = section.content || "";
return event;
}
@ -176,39 +167,47 @@ function createContentEvent(section: any, documentParsed: any, ndk: NDK): NDKEve @@ -176,39 +167,47 @@ function createContentEvent(section: any, documentParsed: any, ndk: NDK): NDKEve
/**
* Detect content type based on parsed structure
*/
function detectContentType(parsed: any): 'article' | 'scattered-notes' | 'none' {
function detectContentType(
parsed: any,
): "article" | "scattered-notes" | "none" {
const hasDocTitle = !!parsed.title;
const hasSections = parsed.sections.length > 0;
// Check if the "title" is actually just the first section title
// This happens when AsciiDoc starts with == instead of =
const titleMatchesFirstSection = parsed.sections.length > 0 &&
parsed.title === parsed.sections[0].title;
const titleMatchesFirstSection =
parsed.sections.length > 0 && parsed.title === parsed.sections[0].title;
if (hasDocTitle && hasSections && !titleMatchesFirstSection) {
return 'article';
return "article";
} else if (hasSections) {
return 'scattered-notes';
return "scattered-notes";
}
return 'none';
return "none";
}
/**
* Generate deterministic d-tag from title
*/
function generateDTag(title: string): string {
return title
.toLowerCase()
.replace(/[^\p{L}\p{N}]/gu, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "") || "untitled";
return (
title
.toLowerCase()
.replace(/[^\p{L}\p{N}]/gu, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "") || "untitled"
);
}
/**
* Add document attributes as Nostr tags
*/
function addDocumentAttributesToTags(tags: string[][], attributes: Record<string, string>, pubkey: string) {
function addDocumentAttributesToTags(
tags: string[][],
attributes: Record<string, string>,
pubkey: string,
) {
// Standard metadata
if (attributes.author) tags.push(["author", attributes.author]);
if (attributes.version) tags.push(["version", attributes.version]);
@ -219,9 +218,7 @@ function addDocumentAttributesToTags(tags: string[][], attributes: Record<string @@ -219,9 +218,7 @@ function addDocumentAttributesToTags(tags: string[][], attributes: Record<string
// Tags
if (attributes.tags) {
attributes.tags.split(',').forEach(tag =>
tags.push(["t", tag.trim()])
);
attributes.tags.split(",").forEach((tag) => tags.push(["t", tag.trim()]));
}
// Add pubkey reference
@ -234,39 +231,90 @@ function addDocumentAttributesToTags(tags: string[][], attributes: Record<string @@ -234,39 +231,90 @@ function addDocumentAttributesToTags(tags: string[][], attributes: Record<string
/**
* Add section-specific attributes as tags
*/
function addSectionAttributesToTags(tags: string[][], attributes: Record<string, string>) {
function addSectionAttributesToTags(
tags: string[][],
attributes: Record<string, string>,
) {
addCustomAttributes(tags, attributes);
}
/**
* Inherit relevant document attributes for content events
*/
function inheritDocumentAttributes(tags: string[][], documentAttributes: Record<string, string>) {
function inheritDocumentAttributes(
tags: string[][],
documentAttributes: Record<string, string>,
) {
// Inherit selected document attributes
if (documentAttributes.language) tags.push(["language", documentAttributes.language]);
if (documentAttributes.language)
tags.push(["language", documentAttributes.language]);
if (documentAttributes.type) tags.push(["type", documentAttributes.type]);
}
/**
* Add custom attributes, filtering out system ones
*/
function addCustomAttributes(tags: string[][], attributes: Record<string, string>) {
function addCustomAttributes(
tags: string[][],
attributes: Record<string, string>,
) {
const systemAttributes = [
'attribute-undefined', 'attribute-missing', 'appendix-caption', 'appendix-refsig',
'caution-caption', 'chapter-refsig', 'example-caption', 'figure-caption',
'important-caption', 'last-update-label', 'manname-title', 'note-caption',
'part-refsig', 'preface-title', 'section-refsig', 'table-caption',
'tip-caption', 'toc-title', 'untitled-label', 'version-label', 'warning-caption',
'asciidoctor', 'asciidoctor-version', 'safe-mode-name', 'backend', 'doctype',
'basebackend', 'filetype', 'outfilesuffix', 'stylesdir', 'iconsdir',
'localdate', 'localyear', 'localtime', 'localdatetime', 'docdate',
'docyear', 'doctime', 'docdatetime', 'doctitle', 'embedded', 'notitle',
"attribute-undefined",
"attribute-missing",
"appendix-caption",
"appendix-refsig",
"caution-caption",
"chapter-refsig",
"example-caption",
"figure-caption",
"important-caption",
"last-update-label",
"manname-title",
"note-caption",
"part-refsig",
"preface-title",
"section-refsig",
"table-caption",
"tip-caption",
"toc-title",
"untitled-label",
"version-label",
"warning-caption",
"asciidoctor",
"asciidoctor-version",
"safe-mode-name",
"backend",
"doctype",
"basebackend",
"filetype",
"outfilesuffix",
"stylesdir",
"iconsdir",
"localdate",
"localyear",
"localtime",
"localdatetime",
"docdate",
"docyear",
"doctime",
"docdatetime",
"doctitle",
"embedded",
"notitle",
// Already handled above
'author', 'version', 'published', 'language', 'image', 'description', 'tags', 'title', 'type'
"author",
"version",
"published",
"language",
"image",
"description",
"tags",
"title",
"type",
];
Object.entries(attributes).forEach(([key, value]) => {
if (!systemAttributes.includes(key) && value && typeof value === 'string') {
if (!systemAttributes.includes(key) && value && typeof value === "string") {
tags.push([key, value]);
}
});
@ -280,16 +328,18 @@ function generateIndexContent(parsed: any): string { @@ -280,16 +328,18 @@ function generateIndexContent(parsed: any): string {
${parsed.sections.length} sections available:
${parsed.sections.map((section: any, i: number) =>
`${i + 1}. ${section.title}`
).join('\n')}`;
${parsed.sections
.map((section: any, i: number) => `${i + 1}. ${section.title}`)
.join("\n")}`;
}
/**
* Export events from PublicationTree for publishing
* This provides compatibility with the current publishing workflow
*/
export async function exportEventsFromTree(result: PublicationTreeFactoryResult) {
export async function exportEventsFromTree(
result: PublicationTreeFactoryResult,
) {
const events: any[] = [];
// Add index event if it exists
@ -298,14 +348,16 @@ export async function exportEventsFromTree(result: PublicationTreeFactoryResult) @@ -298,14 +348,16 @@ export async function exportEventsFromTree(result: PublicationTreeFactoryResult)
}
// Add content events
result.contentEvents.forEach(event => {
result.contentEvents.forEach((event) => {
events.push(eventToPublishableObject(event));
});
return {
indexEvent: result.indexEvent ? eventToPublishableObject(result.indexEvent) : undefined,
indexEvent: result.indexEvent
? eventToPublishableObject(result.indexEvent)
: undefined,
contentEvents: result.contentEvents.map(eventToPublishableObject),
tree: result.tree
tree: result.tree,
};
}
@ -320,6 +372,6 @@ function eventToPublishableObject(event: NDKEvent) { @@ -320,6 +372,6 @@ function eventToPublishableObject(event: NDKEvent) {
created_at: event.created_at,
pubkey: event.pubkey,
id: event.id,
title: event.tags.find(t => t[0] === 'title')?.[1] || 'Untitled'
title: event.tags.find((t) => t[0] === "title")?.[1] || "Untitled",
};
}

59
src/routes/new/compose/+page.svelte

@ -1,8 +1,6 @@ @@ -1,8 +1,6 @@
<script lang="ts">
import { Heading, Button, Alert } from "flowbite-svelte";
import { PaperPlaneOutline } from "flowbite-svelte-icons";
import ZettelEditor from "$lib/components/ZettelEditor.svelte";
import { goto } from "$app/navigation";
import { nip19 } from "nostr-tools";
import { publishSingleEvent } from "$lib/services/publisher";
import { getNdkContext } from "$lib/ndk";
@ -50,14 +48,17 @@ @@ -50,14 +48,17 @@
// Publish index event first using publishSingleEvent
if (events.indexEvent) {
const indexResult = await publishSingleEvent({
content: events.indexEvent.content,
kind: events.indexEvent.kind,
tags: events.indexEvent.tags,
onError: (error) => {
console.error("Index event publish failed:", error);
const indexResult = await publishSingleEvent(
{
content: events.indexEvent.content,
kind: events.indexEvent.kind,
tags: events.indexEvent.tags,
onError: (error) => {
console.error("Index event publish failed:", error);
},
},
}, ndk);
ndk,
);
results.push(indexResult);
}
@ -67,14 +68,17 @@ @@ -67,14 +68,17 @@
console.log(
`Publishing content event ${i + 1}: ${event.tags.find((t: any) => t[0] === "title")?.[1] || "Untitled"}`,
);
const result = await publishSingleEvent({
content: event.content,
kind: event.kind,
tags: event.tags,
onError: (error) => {
console.error(`Content event ${i + 1} publish failed:`, error);
const result = await publishSingleEvent(
{
content: event.content,
kind: event.kind,
tags: event.tags,
onError: (error) => {
console.error(`Content event ${i + 1} publish failed:`, error);
},
},
}, ndk);
ndk,
);
results.push(result);
}
@ -175,14 +179,17 @@ @@ -175,14 +179,17 @@
// Publish only content events for scattered notes
for (let i = 0; i < events.contentEvents.length; i++) {
const event = events.contentEvents[i];
const result = await publishSingleEvent({
content: event.content,
kind: event.kind,
tags: event.tags,
onError: (error) => {
console.error(`Content event ${i + 1} publish failed:`, error);
const result = await publishSingleEvent(
{
content: event.content,
kind: event.kind,
tags: event.tags,
onError: (error) => {
console.error(`Content event ${i + 1} publish failed:`, error);
},
},
}, ndk);
ndk,
);
results.push(result);
}
@ -254,7 +261,7 @@ @@ -254,7 +261,7 @@
// Find the failed event to retry
const failedEvent = publishResults.failedEvents.find(
(event) => event.sectionIndex === sectionIndex
(event) => event.sectionIndex === sectionIndex,
);
if (!failedEvent) return;
@ -265,7 +272,9 @@ @@ -265,7 +272,9 @@
// Retry publishing the failed content
// Note: This is a simplified retry - in production you'd want to store the original event data
// For now, we'll just show an error message
console.error("Retry not implemented - would need to store original event data");
console.error(
"Retry not implemented - would need to store original event data",
);
// Just return early since retry is not implemented
isPublishing = false;
return;

4
tests/unit/metadataExtraction.test.ts

@ -4,8 +4,10 @@ import { @@ -4,8 +4,10 @@ import {
extractSectionMetadata,
extractSmartMetadata,
metadataToTags,
parseAsciiDocWithMetadata,
} from "../../src/lib/utils/asciidoc_metadata.ts";
import {
parseAsciiDocWithMetadata,
} from "../../src/lib/utils/asciidoc_parser.ts";
describe("AsciiDoc Metadata Extraction", () => {
const testContent = `= Test Document with Metadata

Loading…
Cancel
Save