@ -1,5 +1,5 @@
import NDK , { NDKEvent } from '@nostr-dev-kit/ndk' ;
import NDK , { NDKEvent } from "@nostr-dev-kit/ndk" ;
import asciidoctor from 'asciidoctor' ;
import asciidoctor from "asciidoctor" ;
import type {
import type {
AbstractBlock ,
AbstractBlock ,
AbstractNode ,
AbstractNode ,
@ -9,11 +9,11 @@ import type {
Extensions ,
Extensions ,
Section ,
Section ,
ProcessorOptions ,
ProcessorOptions ,
} from 'asciidoctor' ;
} from "asciidoctor" ;
import he from 'he' ;
import he from "he" ;
import { writable , type Writable } from 'svelte/store' ;
import { writable , type Writable } from "svelte/store" ;
import { zettelKinds } from './consts.ts' ;
import { zettelKinds } from "./consts.ts" ;
import { getMatchingTags } from '$lib/utils/nostrUtils' ;
import { getMatchingTags } from "$lib/utils/nostrUtils" ;
interface IndexMetadata {
interface IndexMetadata {
authors? : string [ ] ;
authors? : string [ ] ;
@ -28,12 +28,12 @@ interface IndexMetadata {
export enum SiblingSearchDirection {
export enum SiblingSearchDirection {
Previous ,
Previous ,
Next
Next ,
}
}
export enum InsertLocation {
export enum InsertLocation {
Before ,
Before ,
After
After ,
}
}
/ * *
/ * *
@ -112,7 +112,10 @@ export default class Pharos {
/ * *
/ * *
* A map of index IDs to the IDs of the nodes they reference .
* A map of index IDs to the IDs of the nodes they reference .
* /
* /
private indexToChildEventsMap : Map < string , Set < string > > = new Map < string , Set < string > > ( ) ;
private indexToChildEventsMap : Map < string , Set < string > > = new Map <
string ,
Set < string >
> ( ) ;
/ * *
/ * *
* A map of node IDs to the Nostr event IDs of the events they generate .
* A map of node IDs to the Nostr event IDs of the events they generate .
@ -160,34 +163,37 @@ export default class Pharos {
* /
* /
private async loadAdvancedExtensions ( ) : Promise < void > {
private async loadAdvancedExtensions ( ) : Promise < void > {
try {
try {
const { createAdvancedExtensions } = await import ( './utils/markup/asciidoctorExtensions' ) ;
const { createAdvancedExtensions } = await import (
"./utils/markup/asciidoctorExtensions"
) ;
const advancedExtensions = createAdvancedExtensions ( ) ;
const advancedExtensions = createAdvancedExtensions ( ) ;
// Note: Extensions merging might not be available in this version
// Note: Extensions merging might not be available in this version
// We'll handle this in the parse method instead
// We'll handle this in the parse method instead
} catch ( error ) {
} catch ( error ) {
console . warn ( 'Advanced extensions not available:' , error ) ;
console . warn ( "Advanced extensions not available:" , error ) ;
}
}
}
}
parse ( content : string , options? : ProcessorOptions | undefined ) : void {
parse ( content : string , options? : ProcessorOptions | undefined ) : void {
// Ensure the content is valid AsciiDoc and has a header and the doctype book
// Ensure the content is valid AsciiDoc and has a header and the doctype book
content = ensureAsciiDocHeader ( content ) ;
content = ensureAsciiDocHeader ( content ) ;
try {
try {
const mergedAttributes = Object . assign (
const mergedAttributes = Object . assign (
{ } ,
{ } ,
options && typeof options . attributes === 'object' ? options . attributes : { } ,
options && typeof options . attributes === "object"
{ 'source-highlighter' : 'highlightjs' }
? options . attributes
: { } ,
{ "source-highlighter" : "highlightjs" } ,
) ;
) ;
this . html = this . asciidoctor . convert ( content , {
this . html = this . asciidoctor . convert ( content , {
. . . options ,
. . . options ,
'extension_registry' : this . pharosExtensions ,
extension_registry : this. pharosExtensions,
attributes : mergedAttributes ,
attributes : mergedAttributes ,
} ) as string | Document | undefined ;
} ) as string | Document | undefined ;
} catch ( error ) {
} catch ( error ) {
console . error ( error ) ;
console . error ( error ) ;
throw new Error ( 'Failed to parse AsciiDoc document.' ) ;
throw new Error ( "Failed to parse AsciiDoc document." ) ;
}
}
}
}
@ -199,10 +205,10 @@ export default class Pharos {
async fetch ( event : NDKEvent | string ) : Promise < void > {
async fetch ( event : NDKEvent | string ) : Promise < void > {
let content : string ;
let content : string ;
if ( typeof event === 'string' ) {
if ( typeof event === "string" ) {
const index = await this . ndk . fetchEvent ( { ids : [ event ] } ) ;
const index = await this . ndk . fetchEvent ( { ids : [ event ] } ) ;
if ( ! index ) {
if ( ! index ) {
throw new Error ( 'Failed to fetch publication.' ) ;
throw new Error ( "Failed to fetch publication." ) ;
}
}
content = await this . getPublicationContent ( index ) ;
content = await this . getPublicationContent ( index ) ;
@ -252,7 +258,7 @@ export default class Pharos {
* @returns The HTML content of the converted document .
* @returns The HTML content of the converted document .
* /
* /
getHtml ( ) : string {
getHtml ( ) : string {
return this . html ? . toString ( ) || '' ;
return this . html ? . toString ( ) || "" ;
}
}
/ * *
/ * *
@ -260,7 +266,7 @@ export default class Pharos {
* @remarks The root index ID may be used to retrieve metadata or children from the root index .
* @remarks The root index ID may be used to retrieve metadata or children from the root index .
* /
* /
getRootIndexId ( ) : string {
getRootIndexId ( ) : string {
return this . normalizeId ( this . rootNodeId ) ? ? '' ;
return this . normalizeId ( this . rootNodeId ) ? ? "" ;
}
}
/ * *
/ * *
@ -268,7 +274,7 @@ export default class Pharos {
* /
* /
getIndexTitle ( id : string ) : string | undefined {
getIndexTitle ( id : string ) : string | undefined {
const section = this . nodes . get ( id ) as Section ;
const section = this . nodes . get ( id ) as Section ;
const title = section . getTitle ( ) ? ? '' ;
const title = section . getTitle ( ) ? ? "" ;
return he . decode ( title ) ;
return he . decode ( title ) ;
}
}
@ -276,16 +282,18 @@ export default class Pharos {
* @returns The IDs of any child indices of the index with the given ID .
* @returns The IDs of any child indices of the index with the given ID .
* /
* /
getChildIndexIds ( id : string ) : string [ ] {
getChildIndexIds ( id : string ) : string [ ] {
return Array . from ( this . indexToChildEventsMap . get ( id ) ? ? [ ] )
return Array . from ( this . indexToChildEventsMap . get ( id ) ? ? [ ] ) . filter (
. filter ( id = > this . eventToKindMap . get ( id ) === 30040 ) ;
( id ) = > this . eventToKindMap . get ( id ) === 30040 ,
) ;
}
}
/ * *
/ * *
* @returns The IDs of any child zettels of the index with the given ID .
* @returns The IDs of any child zettels of the index with the given ID .
* /
* /
getChildZettelIds ( id : string ) : string [ ] {
getChildZettelIds ( id : string ) : string [ ] {
return Array . from ( this . indexToChildEventsMap . get ( id ) ? ? [ ] )
return Array . from ( this . indexToChildEventsMap . get ( id ) ? ? [ ] ) . filter (
. filter ( id = > this . eventToKindMap . get ( id ) !== 30040 ) ;
( id ) = > this . eventToKindMap . get ( id ) !== 30040 ,
) ;
}
}
/ * *
/ * *
@ -307,8 +315,8 @@ export default class Pharos {
const block = this . nodes . get ( normalizedId ! ) as AbstractBlock ;
const block = this . nodes . get ( normalizedId ! ) as AbstractBlock ;
switch ( block . getContext ( ) ) {
switch ( block . getContext ( ) ) {
case 'paragraph' :
case "paragraph" :
return block . getContent ( ) ? ? '' ;
return block . getContent ( ) ? ? "" ;
}
}
return block . convert ( ) ;
return block . convert ( ) ;
@ -326,7 +334,7 @@ export default class Pharos {
}
}
const context = this . eventToContextMap . get ( normalizedId ) ;
const context = this . eventToContextMap . get ( normalizedId ) ;
return context === 'floating_title' ;
return context === "floating_title" ;
}
}
/ * *
/ * *
@ -361,7 +369,7 @@ export default class Pharos {
getNearestSibling (
getNearestSibling (
targetDTag : string ,
targetDTag : string ,
depth : number ,
depth : number ,
direction : SiblingSearchDirection
direction : SiblingSearchDirection ,
) : [ string | null , string | null ] {
) : [ string | null , string | null ] {
const eventsAtLevel = this . eventsByLevelMap . get ( depth ) ;
const eventsAtLevel = this . eventsByLevelMap . get ( depth ) ;
if ( ! eventsAtLevel ) {
if ( ! eventsAtLevel ) {
@ -371,13 +379,17 @@ export default class Pharos {
const targetIndex = eventsAtLevel . indexOf ( targetDTag ) ;
const targetIndex = eventsAtLevel . indexOf ( targetDTag ) ;
if ( targetIndex === - 1 ) {
if ( targetIndex === - 1 ) {
throw new Error ( ` The event indicated by #d: ${ targetDTag } does not exist at level ${ depth } of the event tree. ` ) ;
throw new Error (
` The event indicated by #d: ${ targetDTag } does not exist at level ${ depth } of the event tree. ` ,
) ;
}
}
const parentDTag = this . getParent ( targetDTag ) ;
const parentDTag = this . getParent ( targetDTag ) ;
if ( ! parentDTag ) {
if ( ! parentDTag ) {
throw new Error ( ` The event indicated by #d: ${ targetDTag } does not have a parent. ` ) ;
throw new Error (
` The event indicated by #d: ${ targetDTag } does not have a parent. ` ,
) ;
}
}
const grandparentDTag = this . getParent ( parentDTag ) ;
const grandparentDTag = this . getParent ( parentDTag ) ;
@ -395,7 +407,10 @@ export default class Pharos {
// If the target is the last node at its level and we're searching for a next sibling,
// If the target is the last node at its level and we're searching for a next sibling,
// look among the siblings of the target's parent at the previous level.
// look among the siblings of the target's parent at the previous level.
if ( targetIndex === eventsAtLevel . length - 1 && direction === SiblingSearchDirection . Next ) {
if (
targetIndex === eventsAtLevel . length - 1 &&
direction === SiblingSearchDirection . Next
) {
// * Base case: The target is at the last level of the tree and has no subsequent sibling.
// * Base case: The target is at the last level of the tree and has no subsequent sibling.
if ( ! grandparentDTag ) {
if ( ! grandparentDTag ) {
return [ null , null ] ;
return [ null , null ] ;
@ -406,10 +421,10 @@ export default class Pharos {
// * Base case: There is an adjacent sibling at the same depth as the target.
// * Base case: There is an adjacent sibling at the same depth as the target.
switch ( direction ) {
switch ( direction ) {
case SiblingSearchDirection . Previous :
case SiblingSearchDirection . Previous :
return [ eventsAtLevel [ targetIndex - 1 ] , parentDTag ] ;
return [ eventsAtLevel [ targetIndex - 1 ] , parentDTag ] ;
case SiblingSearchDirection . Next :
case SiblingSearchDirection . Next :
return [ eventsAtLevel [ targetIndex + 1 ] , parentDTag ] ;
return [ eventsAtLevel [ targetIndex + 1 ] , parentDTag ] ;
}
}
return [ null , null ] ;
return [ null , null ] ;
@ -424,7 +439,9 @@ export default class Pharos {
getParent ( dTag : string ) : string | null {
getParent ( dTag : string ) : string | null {
// Check if the event exists in the parser tree.
// Check if the event exists in the parser tree.
if ( ! this . eventIds . has ( dTag ) ) {
if ( ! this . eventIds . has ( dTag ) ) {
throw new Error ( ` The event indicated by #d: ${ dTag } does not exist in the parser tree. ` ) ;
throw new Error (
` The event indicated by #d: ${ dTag } does not exist in the parser tree. ` ,
) ;
}
}
// Iterate through all the index to child mappings.
// Iterate through all the index to child mappings.
@ -449,7 +466,11 @@ export default class Pharos {
* @remarks Moving the target event within the tree changes the hash of several events , so the
* @remarks Moving the target event within the tree changes the hash of several events , so the
* event tree will be regenerated when the consumer next invokes ` getEvents() ` .
* event tree will be regenerated when the consumer next invokes ` getEvents() ` .
* /
* /
moveEvent ( targetDTag : string , destinationDTag : string , insertAfter : boolean = false ) : void {
moveEvent (
targetDTag : string ,
destinationDTag : string ,
insertAfter : boolean = false ,
) : void {
const targetEvent = this . events . get ( targetDTag ) ;
const targetEvent = this . events . get ( targetDTag ) ;
const destinationEvent = this . events . get ( destinationDTag ) ;
const destinationEvent = this . events . get ( destinationDTag ) ;
const targetParent = this . getParent ( targetDTag ) ;
const targetParent = this . getParent ( targetDTag ) ;
@ -464,11 +485,15 @@ export default class Pharos {
}
}
if ( ! targetParent ) {
if ( ! targetParent ) {
throw new Error ( ` The event indicated by #d: ${ targetDTag } does not have a parent. ` ) ;
throw new Error (
` The event indicated by #d: ${ targetDTag } does not have a parent. ` ,
) ;
}
}
if ( ! destinationParent ) {
if ( ! destinationParent ) {
throw new Error ( ` The event indicated by #d: ${ destinationDTag } does not have a parent. ` ) ;
throw new Error (
` The event indicated by #d: ${ destinationDTag } does not have a parent. ` ,
) ;
}
}
// Remove the target from among the children of its current parent.
// Remove the target from among the children of its current parent.
@ -478,16 +503,22 @@ export default class Pharos {
this . indexToChildEventsMap . get ( destinationParent ) ? . delete ( targetDTag ) ;
this . indexToChildEventsMap . get ( destinationParent ) ? . delete ( targetDTag ) ;
// Get the index of the destination event among the children of its parent.
// Get the index of the destination event among the children of its parent.
const destinationIndex = Array . from ( this . indexToChildEventsMap . get ( destinationParent ) ? ? [ ] )
const destinationIndex = Array . from (
. indexOf ( destinationDTag ) ;
this . indexToChildEventsMap . get ( destinationParent ) ? ? [ ] ,
) . indexOf ( destinationDTag ) ;
// Insert next to the index of the destination event, either before or after as specified by
// Insert next to the index of the destination event, either before or after as specified by
// the insertAfter flag.
// the insertAfter flag.
const destinationChildren = Array . from ( this . indexToChildEventsMap . get ( destinationParent ) ? ? [ ] ) ;
const destinationChildren = Array . from (
this . indexToChildEventsMap . get ( destinationParent ) ? ? [ ] ,
) ;
insertAfter
insertAfter
? destinationChildren . splice ( destinationIndex + 1 , 0 , targetDTag )
? destinationChildren . splice ( destinationIndex + 1 , 0 , targetDTag )
: destinationChildren . splice ( destinationIndex , 0 , targetDTag ) ;
: destinationChildren . splice ( destinationIndex , 0 , targetDTag ) ;
this . indexToChildEventsMap . set ( destinationParent , new Set ( destinationChildren ) ) ;
this . indexToChildEventsMap . set (
destinationParent ,
new Set ( destinationChildren ) ,
) ;
this . shouldUpdateEventTree = true ;
this . shouldUpdateEventTree = true ;
}
}
@ -517,7 +548,10 @@ export default class Pharos {
* - Each node ID is mapped to an integer event kind that will be used to represent the node .
* - Each node ID is mapped to an integer event kind that will be used to represent the node .
* - Each ID of a node containing children is mapped to the set of IDs of its children .
* - Each ID of a node containing children is mapped to the set of IDs of its children .
* /
* /
private treeProcessor ( treeProcessor : Extensions.TreeProcessor , document : Document ) {
private treeProcessor (
treeProcessor : Extensions.TreeProcessor ,
document : Document ,
) {
this . rootNodeId = this . generateNodeId ( document ) ;
this . rootNodeId = this . generateNodeId ( document ) ;
document . setId ( this . rootNodeId ) ;
document . setId ( this . rootNodeId ) ;
this . nodes . set ( this . rootNodeId , document ) ;
this . nodes . set ( this . rootNodeId , document ) ;
@ -533,7 +567,7 @@ export default class Pharos {
continue ;
continue ;
}
}
if ( block . getContext ( ) === 'section' ) {
if ( block . getContext ( ) === "section" ) {
const children = this . processSection ( block as Section ) ;
const children = this . processSection ( block as Section ) ;
nodeQueue . push ( . . . children ) ;
nodeQueue . push ( . . . children ) ;
} else {
} else {
@ -563,7 +597,7 @@ export default class Pharos {
}
}
this . nodes . set ( sectionId , section ) ;
this . nodes . set ( sectionId , section ) ;
this . eventToKindMap . set ( sectionId , 30040 ) ; // Sections are indexToChildEventsMap by default.
this . eventToKindMap . set ( sectionId , 30040 ) ; // Sections are indexToChildEventsMap by default.
this . indexToChildEventsMap . set ( sectionId , new Set < string > ( ) ) ;
this . indexToChildEventsMap . set ( sectionId , new Set < string > ( ) ) ;
const parentId = this . normalizeId ( section . getParent ( ) ? . getId ( ) ) ;
const parentId = this . normalizeId ( section . getParent ( ) ? . getId ( ) ) ;
@ -591,7 +625,7 @@ export default class Pharos {
// Obtain or generate a unique ID for the block.
// Obtain or generate a unique ID for the block.
let blockId = this . normalizeId ( block . getId ( ) ) ;
let blockId = this . normalizeId ( block . getId ( ) ) ;
if ( ! blockId ) {
if ( ! blockId ) {
blockId = this . generateNodeId ( block ) ;
blockId = this . generateNodeId ( block ) ;
block . setId ( blockId ) ;
block . setId ( blockId ) ;
}
}
@ -601,7 +635,7 @@ export default class Pharos {
}
}
this . nodes . set ( blockId , block ) ;
this . nodes . set ( blockId , block ) ;
this . eventToKindMap . set ( blockId , 30041 ) ; // Blocks are zettels by default.
this . eventToKindMap . set ( blockId , 30041 ) ; // Blocks are zettels by default.
const parentId = this . normalizeId ( block . getParent ( ) ? . getId ( ) ) ;
const parentId = this . normalizeId ( block . getParent ( ) ? . getId ( ) ) ;
if ( ! parentId ) {
if ( ! parentId ) {
@ -648,21 +682,24 @@ export default class Pharos {
* @remarks This function does a depth - first crawl of the event tree using the relays specified
* @remarks This function does a depth - first crawl of the event tree using the relays specified
* on the NDK instance .
* on the NDK instance .
* /
* /
private async getPublicationContent ( event : NDKEvent , depth : number = 0 ) : Promise < string > {
private async getPublicationContent (
let content : string = '' ;
event : NDKEvent ,
depth : number = 0 ,
) : Promise < string > {
let content : string = "" ;
// Format title into AsciiDoc header.
// Format title into AsciiDoc header.
const title = getMatchingTags ( event , 'title' ) [ 0 ] [ 1 ] ;
const title = getMatchingTags ( event , "title" ) [ 0 ] [ 1 ] ;
let titleLevel = '' ;
let titleLevel = "" ;
for ( let i = 0 ; i <= depth ; i ++ ) {
for ( let i = 0 ; i <= depth ; i ++ ) {
titleLevel += '=' ;
titleLevel += "=" ;
}
}
content += ` ${ titleLevel } ${ title } \ n \ n ` ;
content += ` ${ titleLevel } ${ title } \ n \ n ` ;
// TODO: Deprecate `e` tags in favor of `a` tags required by NIP-62.
// TODO: Deprecate `e` tags in favor of `a` tags required by NIP-62.
let tags = getMatchingTags ( event , 'a' ) ;
let tags = getMatchingTags ( event , "a" ) ;
if ( tags . length === 0 ) {
if ( tags . length === 0 ) {
tags = getMatchingTags ( event , 'e' ) ;
tags = getMatchingTags ( event , "e" ) ;
}
}
// Base case: The event is a zettel.
// Base case: The event is a zettel.
@ -673,24 +710,29 @@ export default class Pharos {
// Recursive case: The event is an index.
// Recursive case: The event is an index.
const childEvents = await Promise . all (
const childEvents = await Promise . all (
tags . map ( tag = > this . ndk . fetchEventFromTag ( tag , event ) )
tags . map ( ( tag ) = > this . ndk . fetchEventFromTag ( tag , event ) ) ,
) ;
) ;
// if a blog, save complete events for later
// if a blog, save complete events for later
if ( getMatchingTags ( event , 'type' ) . length > 0 && getMatchingTags ( event , 'type' ) [ 0 ] [ 1 ] === 'blog' ) {
if (
childEvents . forEach ( child = > {
getMatchingTags ( event , "type" ) . length > 0 &&
getMatchingTags ( event , "type" ) [ 0 ] [ 1 ] === "blog"
) {
childEvents . forEach ( ( child ) = > {
if ( child ) {
if ( child ) {
this . blogEntries . set ( getMatchingTags ( child , 'd' ) ? . [ 0 ] ? . [ 1 ] , child ) ;
this . blogEntries . set ( getMatchingTags ( child , "d" ) ? . [ 0 ] ? . [ 1 ] , child ) ;
}
}
} )
} ) ;
}
}
// populate metadata
// populate metadata
if ( event . created_at ) {
if ( event . created_at ) {
this . rootIndexMetadata . publicationDate = new Date ( event . created_at * 1000 ) . toDateString ( ) ;
this . rootIndexMetadata . publicationDate = new Date (
event . created_at * 1000 ,
) . toDateString ( ) ;
}
}
if ( getMatchingTags ( event , 'image' ) . length > 0 ) {
if ( getMatchingTags ( event , "image" ) . length > 0 ) {
this . rootIndexMetadata . coverImage = getMatchingTags ( event , 'image' ) [ 0 ] [ 1 ] ;
this . rootIndexMetadata . coverImage = getMatchingTags ( event , "image" ) [ 0 ] [ 1 ] ;
}
}
// Michael J - 15 December 2024 - This could be further parallelized by recursively fetching
// Michael J - 15 December 2024 - This could be further parallelized by recursively fetching
@ -705,11 +747,13 @@ export default class Pharos {
continue ;
continue ;
}
}
childContentPromises . push ( this . getPublicationContent ( childEvent , depth + 1 ) ) ;
childContentPromises . push (
this . getPublicationContent ( childEvent , depth + 1 ) ,
) ;
}
}
const childContents = await Promise . all ( childContentPromises ) ;
const childContents = await Promise . all ( childContentPromises ) ;
content += childContents . join ( '\n\n' ) ;
content += childContents . join ( "\n\n" ) ;
return content ;
return content ;
}
}
@ -756,15 +800,15 @@ export default class Pharos {
const nodeId = nodeIdStack . pop ( ) ;
const nodeId = nodeIdStack . pop ( ) ;
switch ( this . eventToKindMap . get ( nodeId ! ) ) {
switch ( this . eventToKindMap . get ( nodeId ! ) ) {
case 30040 :
case 30040 :
events . push ( this . generateIndexEvent ( nodeId ! , pubkey ) ) ;
events . push ( this . generateIndexEvent ( nodeId ! , pubkey ) ) ;
break ;
break ;
case 30041 :
case 30041 :
default :
default :
// Kind 30041 (zettel) is currently the default kind for contentful events.
// Kind 30041 (zettel) is currently the default kind for contentful events.
events . push ( this . generateZettelEvent ( nodeId ! , pubkey ) ) ;
events . push ( this . generateZettelEvent ( nodeId ! , pubkey ) ) ;
break ;
break ;
}
}
}
}
@ -783,17 +827,14 @@ export default class Pharos {
private generateIndexEvent ( nodeId : string , pubkey : string ) : NDKEvent {
private generateIndexEvent ( nodeId : string , pubkey : string ) : NDKEvent {
const title = ( this . nodes . get ( nodeId ) ! as AbstractBlock ) . getTitle ( ) ;
const title = ( this . nodes . get ( nodeId ) ! as AbstractBlock ) . getTitle ( ) ;
// TODO: Use a tags as per NIP-62.
// TODO: Use a tags as per NIP-62.
const childTags = Array . from ( this . indexToChildEventsMap . get ( nodeId ) ! )
const childTags = Array . from ( this . indexToChildEventsMap . get ( nodeId ) ! ) . map (
. map ( id = > [ '#e' , this . eventIds . get ( id ) ! ] ) ;
( id ) = > [ "#e" , this . eventIds . get ( id ) ! ] ,
) ;
const event = new NDKEvent ( this . ndk ) ;
const event = new NDKEvent ( this . ndk ) ;
event . kind = 30040 ;
event . kind = 30040 ;
event . content = '' ;
event . content = "" ;
event . tags = [
event . tags = [ [ "title" , title ! ] , [ "#d" , nodeId ] , . . . childTags ] ;
[ 'title' , title ! ] ,
[ '#d' , nodeId ] ,
. . . childTags
] ;
event . created_at = Date . now ( ) ;
event . created_at = Date . now ( ) ;
event . pubkey = pubkey ;
event . pubkey = pubkey ;
@ -805,7 +846,7 @@ export default class Pharos {
this . rootIndexMetadata = {
this . rootIndexMetadata = {
authors : document
authors : document
. getAuthors ( )
. getAuthors ( )
. map ( author = > author . getName ( ) )
. map ( ( author ) = > author . getName ( ) )
. filter ( ( name ) : name is string = > name != null ) ,
. filter ( ( name ) : name is string = > name != null ) ,
version : document.getRevisionNumber ( ) ,
version : document.getRevisionNumber ( ) ,
edition : document.getRevisionRemark ( ) ,
edition : document.getRevisionRemark ( ) ,
@ -813,11 +854,11 @@ export default class Pharos {
} ;
} ;
if ( this . rootIndexMetadata . authors ) {
if ( this . rootIndexMetadata . authors ) {
event . tags . push ( [ 'author' , . . . this . rootIndexMetadata . authors ! ] ) ;
event . tags . push ( [ "author" , . . . this . rootIndexMetadata . authors ! ] ) ;
}
}
if ( this . rootIndexMetadata . version || this . rootIndexMetadata . edition ) {
if ( this . rootIndexMetadata . version || this . rootIndexMetadata . edition ) {
const versionTags : string [ ] = [ 'version' ] ;
const versionTags : string [ ] = [ "version" ] ;
if ( this . rootIndexMetadata . version ) {
if ( this . rootIndexMetadata . version ) {
versionTags . push ( this . rootIndexMetadata . version ) ;
versionTags . push ( this . rootIndexMetadata . version ) ;
}
}
@ -828,7 +869,10 @@ export default class Pharos {
}
}
if ( this . rootIndexMetadata . publicationDate ) {
if ( this . rootIndexMetadata . publicationDate ) {
event . tags . push ( [ 'published_on' , this . rootIndexMetadata . publicationDate ! ] ) ;
event . tags . push ( [
"published_on" ,
this . rootIndexMetadata . publicationDate ! ,
] ) ;
}
}
}
}
@ -852,14 +896,14 @@ export default class Pharos {
* /
* /
private generateZettelEvent ( nodeId : string , pubkey : string ) : NDKEvent {
private generateZettelEvent ( nodeId : string , pubkey : string ) : NDKEvent {
const title = ( this . nodes . get ( nodeId ) ! as Block ) . getTitle ( ) ;
const title = ( this . nodes . get ( nodeId ) ! as Block ) . getTitle ( ) ;
const content = ( this . nodes . get ( nodeId ) ! as Block ) . getSource ( ) ; // AsciiDoc source content.
const content = ( this . nodes . get ( nodeId ) ! as Block ) . getSource ( ) ; // AsciiDoc source content.
const event = new NDKEvent ( this . ndk ) ;
const event = new NDKEvent ( this . ndk ) ;
event . kind = 30041 ;
event . kind = 30041 ;
event . content = content ! ;
event . content = content ! ;
event . tags = [
event . tags = [
[ 'title' , title ! ] ,
[ "title" , title ! ] ,
[ '#d' , nodeId ] ,
[ "#d" , nodeId ] ,
. . . this . extractAndNormalizeWikilinks ( content ! ) ,
. . . this . extractAndNormalizeWikilinks ( content ! ) ,
] ;
] ;
event . created_at = Date . now ( ) ;
event . created_at = Date . now ( ) ;
@ -902,173 +946,173 @@ export default class Pharos {
const context = block . getContext ( ) ;
const context = block . getContext ( ) ;
switch ( context ) {
switch ( context ) {
case 'admonition' :
case "admonition" :
blockNumber = this . contextCounters . get ( 'admonition' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "admonition" ) ? ? 0 ;
blockId = ` ${ documentId } -admonition- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -admonition- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'admonition' , blockNumber ) ;
this . contextCounters . set ( "admonition" , blockNumber ) ;
break ;
break ;
case 'audio' :
case "audio" :
blockNumber = this . contextCounters . get ( 'audio' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "audio" ) ? ? 0 ;
blockId = ` ${ documentId } -audio- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -audio- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'audio' , blockNumber ) ;
this . contextCounters . set ( "audio" , blockNumber ) ;
break ;
break ;
case 'colist' :
case "colist" :
blockNumber = this . contextCounters . get ( 'colist' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "colist" ) ? ? 0 ;
blockId = ` ${ documentId } -colist- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -colist- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'colist' , blockNumber ) ;
this . contextCounters . set ( "colist" , blockNumber ) ;
break ;
break ;
case 'dlist' :
case "dlist" :
blockNumber = this . contextCounters . get ( 'dlist' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "dlist" ) ? ? 0 ;
blockId = ` ${ documentId } -dlist- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -dlist- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'dlist' , blockNumber ) ;
this . contextCounters . set ( "dlist" , blockNumber ) ;
break ;
break ;
case 'document' :
case "document" :
blockNumber = this . contextCounters . get ( 'document' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "document" ) ? ? 0 ;
blockId = ` ${ documentId } -document- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -document- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'document' , blockNumber ) ;
this . contextCounters . set ( "document" , blockNumber ) ;
break ;
break ;
case 'example' :
case "example" :
blockNumber = this . contextCounters . get ( 'example' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "example" ) ? ? 0 ;
blockId = ` ${ documentId } -example- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -example- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'example' , blockNumber ) ;
this . contextCounters . set ( "example" , blockNumber ) ;
break ;
break ;
case 'floating_title' :
case "floating_title" :
blockNumber = this . contextCounters . get ( 'floating_title' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "floating_title" ) ? ? 0 ;
blockId = ` ${ documentId } -floating-title- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -floating-title- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'floating_title' , blockNumber ) ;
this . contextCounters . set ( "floating_title" , blockNumber ) ;
break ;
break ;
case 'image' :
case "image" :
blockNumber = this . contextCounters . get ( 'image' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "image" ) ? ? 0 ;
blockId = ` ${ documentId } -image- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -image- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'image' , blockNumber ) ;
this . contextCounters . set ( "image" , blockNumber ) ;
break ;
break ;
case 'list_item' :
case "list_item" :
blockNumber = this . contextCounters . get ( 'list_item' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "list_item" ) ? ? 0 ;
blockId = ` ${ documentId } -list-item- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -list-item- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'list_item' , blockNumber ) ;
this . contextCounters . set ( "list_item" , blockNumber ) ;
break ;
break ;
case 'listing' :
case "listing" :
blockNumber = this . contextCounters . get ( 'listing' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "listing" ) ? ? 0 ;
blockId = ` ${ documentId } -listing- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -listing- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'listing' , blockNumber ) ;
this . contextCounters . set ( "listing" , blockNumber ) ;
break ;
break ;
case 'literal' :
case "literal" :
blockNumber = this . contextCounters . get ( 'literal' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "literal" ) ? ? 0 ;
blockId = ` ${ documentId } -literal- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -literal- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'literal' , blockNumber ) ;
this . contextCounters . set ( "literal" , blockNumber ) ;
break ;
break ;
case 'olist' :
case "olist" :
blockNumber = this . contextCounters . get ( 'olist' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "olist" ) ? ? 0 ;
blockId = ` ${ documentId } -olist- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -olist- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'olist' , blockNumber ) ;
this . contextCounters . set ( "olist" , blockNumber ) ;
break ;
break ;
case 'open' :
case "open" :
blockNumber = this . contextCounters . get ( 'open' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "open" ) ? ? 0 ;
blockId = ` ${ documentId } -open- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -open- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'open' , blockNumber ) ;
this . contextCounters . set ( "open" , blockNumber ) ;
break ;
break ;
case 'page_break' :
case "page_break" :
blockNumber = this . contextCounters . get ( 'page_break' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "page_break" ) ? ? 0 ;
blockId = ` ${ documentId } -page-break- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -page-break- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'page_break' , blockNumber ) ;
this . contextCounters . set ( "page_break" , blockNumber ) ;
break ;
break ;
case 'paragraph' :
case "paragraph" :
blockNumber = this . contextCounters . get ( 'paragraph' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "paragraph" ) ? ? 0 ;
blockId = ` ${ documentId } -paragraph- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -paragraph- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'paragraph' , blockNumber ) ;
this . contextCounters . set ( "paragraph" , blockNumber ) ;
break ;
break ;
case 'pass' :
case "pass" :
blockNumber = this . contextCounters . get ( 'pass' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "pass" ) ? ? 0 ;
blockId = ` ${ documentId } -pass- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -pass- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'pass' , blockNumber ) ;
this . contextCounters . set ( "pass" , blockNumber ) ;
break ;
break ;
case 'preamble' :
case "preamble" :
blockNumber = this . contextCounters . get ( 'preamble' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "preamble" ) ? ? 0 ;
blockId = ` ${ documentId } -preamble- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -preamble- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'preamble' , blockNumber ) ;
this . contextCounters . set ( "preamble" , blockNumber ) ;
break ;
break ;
case 'quote' :
case "quote" :
blockNumber = this . contextCounters . get ( 'quote' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "quote" ) ? ? 0 ;
blockId = ` ${ documentId } -quote- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -quote- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'quote' , blockNumber ) ;
this . contextCounters . set ( "quote" , blockNumber ) ;
break ;
break ;
case 'section' :
case "section" :
blockNumber = this . contextCounters . get ( 'section' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "section" ) ? ? 0 ;
blockId = ` ${ documentId } -section- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -section- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'section' , blockNumber ) ;
this . contextCounters . set ( "section" , blockNumber ) ;
break ;
break ;
case 'sidebar' :
case "sidebar" :
blockNumber = this . contextCounters . get ( 'sidebar' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "sidebar" ) ? ? 0 ;
blockId = ` ${ documentId } -sidebar- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -sidebar- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'sidebar' , blockNumber ) ;
this . contextCounters . set ( "sidebar" , blockNumber ) ;
break ;
break ;
case 'table' :
case "table" :
blockNumber = this . contextCounters . get ( 'table' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "table" ) ? ? 0 ;
blockId = ` ${ documentId } -table- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -table- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'table' , blockNumber ) ;
this . contextCounters . set ( "table" , blockNumber ) ;
break ;
break ;
case 'table_cell' :
case "table_cell" :
blockNumber = this . contextCounters . get ( 'table_cell' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "table_cell" ) ? ? 0 ;
blockId = ` ${ documentId } -table-cell- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -table-cell- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'table_cell' , blockNumber ) ;
this . contextCounters . set ( "table_cell" , blockNumber ) ;
break ;
break ;
case 'thematic_break' :
case "thematic_break" :
blockNumber = this . contextCounters . get ( 'thematic_break' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "thematic_break" ) ? ? 0 ;
blockId = ` ${ documentId } -thematic-break- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -thematic-break- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'thematic_break' , blockNumber ) ;
this . contextCounters . set ( "thematic_break" , blockNumber ) ;
break ;
break ;
case 'toc' :
case "toc" :
blockNumber = this . contextCounters . get ( 'toc' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "toc" ) ? ? 0 ;
blockId = ` ${ documentId } -toc- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -toc- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'toc' , blockNumber ) ;
this . contextCounters . set ( "toc" , blockNumber ) ;
break ;
break ;
case 'ulist' :
case "ulist" :
blockNumber = this . contextCounters . get ( 'ulist' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "ulist" ) ? ? 0 ;
blockId = ` ${ documentId } -ulist- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -ulist- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'ulist' , blockNumber ) ;
this . contextCounters . set ( "ulist" , blockNumber ) ;
break ;
break ;
case 'verse' :
case "verse" :
blockNumber = this . contextCounters . get ( 'verse' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "verse" ) ? ? 0 ;
blockId = ` ${ documentId } -verse- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -verse- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'verse' , blockNumber ) ;
this . contextCounters . set ( "verse" , blockNumber ) ;
break ;
break ;
case 'video' :
case "video" :
blockNumber = this . contextCounters . get ( 'video' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "video" ) ? ? 0 ;
blockId = ` ${ documentId } -video- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -video- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'video' , blockNumber ) ;
this . contextCounters . set ( "video" , blockNumber ) ;
break ;
break ;
default :
default :
blockNumber = this . contextCounters . get ( 'block' ) ? ? 0 ;
blockNumber = this . contextCounters . get ( "block" ) ? ? 0 ;
blockId = ` ${ documentId } -block- ${ blockNumber ++ } ` ;
blockId = ` ${ documentId } -block- ${ blockNumber ++ } ` ;
this . contextCounters . set ( 'block' , blockNumber ) ;
this . contextCounters . set ( "block" , blockNumber ) ;
break ;
break ;
}
}
block . setId ( blockId ) ;
block . setId ( blockId ) ;
@ -1082,24 +1126,25 @@ export default class Pharos {
return null ;
return null ;
}
}
return he . decode ( input )
return he
. decode ( input )
. toLowerCase ( )
. toLowerCase ( )
. replace ( /[_]/g , ' ' ) // Replace underscores with spaces.
. replace ( /[_]/g , " " ) // Replace underscores with spaces.
. trim ( )
. trim ( )
. replace ( /\s+/g , '-' ) // Replace spaces with dashes.
. replace ( /\s+/g , "-" ) // Replace spaces with dashes.
. replace ( /[^a-z0-9\-]/g , '' ) ; // Remove non-alphanumeric characters except dashes.
. replace ( /[^a-z0-9\-]/g , "" ) ; // Remove non-alphanumeric characters except dashes.
}
}
private updateEventByContext ( dTag : string , value : string , context : string ) {
private updateEventByContext ( dTag : string , value : string , context : string ) {
switch ( context ) {
switch ( context ) {
case 'document' :
case "document" :
case 'section' :
case "section" :
this . updateEventTitle ( dTag , value ) ;
this . updateEventTitle ( dTag , value ) ;
break ;
break ;
default :
default :
this . updateEventBody ( dTag , value ) ;
this . updateEventBody ( dTag , value ) ;
break ;
break ;
}
}
}
}
@ -1131,7 +1176,7 @@ export default class Pharos {
while ( ( match = wikilinkPattern . exec ( content ) ) !== null ) {
while ( ( match = wikilinkPattern . exec ( content ) ) !== null ) {
const linkName = match [ 1 ] ;
const linkName = match [ 1 ] ;
const normalizedText = this . normalizeId ( linkName ) ;
const normalizedText = this . normalizeId ( linkName ) ;
wikilinks . push ( [ 'wikilink' , normalizedText ! ] ) ;
wikilinks . push ( [ "wikilink" , normalizedText ! ] ) ;
}
}
return wikilinks ;
return wikilinks ;
@ -1147,7 +1192,7 @@ export const pharosInstance: Writable<Pharos> = writable();
export const tocUpdate = writable ( 0 ) ;
export const tocUpdate = writable ( 0 ) ;
// Whenever you update the publication tree, call:
// Whenever you update the publication tree, call:
tocUpdate . update ( n = > n + 1 ) ;
tocUpdate . update ( ( n ) = > n + 1 ) ;
function ensureAsciiDocHeader ( content : string ) : string {
function ensureAsciiDocHeader ( content : string ) : string {
const lines = content . split ( /\r?\n/ ) ;
const lines = content . split ( /\r?\n/ ) ;
@ -1156,35 +1201,36 @@ function ensureAsciiDocHeader(content: string): string {
// Find the first non-empty line as header
// Find the first non-empty line as header
for ( let i = 0 ; i < lines . length ; i ++ ) {
for ( let i = 0 ; i < lines . length ; i ++ ) {
if ( lines [ i ] . trim ( ) === '' ) continue ;
if ( lines [ i ] . trim ( ) === "" ) continue ;
if ( lines [ i ] . trim ( ) . startsWith ( '=' ) ) {
if ( lines [ i ] . trim ( ) . startsWith ( "=" ) ) {
headerIndex = i ;
headerIndex = i ;
break ;
break ;
} else {
} else {
throw new Error ( 'AsciiDoc document is missing a header at the top.' ) ;
throw new Error ( "AsciiDoc document is missing a header at the top." ) ;
}
}
}
}
if ( headerIndex === - 1 ) {
if ( headerIndex === - 1 ) {
throw new Error ( 'AsciiDoc document is missing a header.' ) ;
throw new Error ( "AsciiDoc document is missing a header." ) ;
}
}
// Check for doctype in the next non-empty line after header
// Check for doctype in the next non-empty line after header
let nextLine = headerIndex + 1 ;
let nextLine = headerIndex + 1 ;
while ( nextLine < lines . length && lines [ nextLine ] . trim ( ) === '' ) {
while ( nextLine < lines . length && lines [ nextLine ] . trim ( ) === "" ) {
nextLine ++ ;
nextLine ++ ;
}
}
if ( nextLine < lines . length && lines [ nextLine ] . trim ( ) . startsWith ( ':doctype:' ) ) {
if (
nextLine < lines . length &&
lines [ nextLine ] . trim ( ) . startsWith ( ":doctype:" )
) {
hasDoctype = true ;
hasDoctype = true ;
}
}
// Insert doctype immediately after header if not present
// Insert doctype immediately after header if not present
if ( ! hasDoctype ) {
if ( ! hasDoctype ) {
lines . splice ( headerIndex + 1 , 0 , ':doctype: book' ) ;
lines . splice ( headerIndex + 1 , 0 , ":doctype: book" ) ;
}
}
return lines . join ( "\n" ) ;
return lines . join ( '\n' ) ;
}
}