11
22import type { Parent , Node , Yaml , ListItem , Text , Heading } from 'mdast' ;
3- import type { TagMap , Task , Worklog , ParseContext , ParseFileContext , InternalTagMap , ParsedHeading } from './types.js' ;
3+ import type { TagMap , Task , Worklog , ParseContext , ParseFileContext } from './types.js' ;
44
55import { readdir , readFile , stat } from 'node:fs/promises' ;
66import { resolve , relative } from 'node:path' ;
@@ -15,116 +15,95 @@ import { gfmFromMarkdown } from 'mdast-util-gfm';
1515import { frontmatterFromMarkdown } from 'mdast-util-frontmatter' ;
1616
1717import { extractTagsFromText , extractTagsFromYaml } from './tags.js' ;
18- import { FOLDER_META_FILE } from './utils.js' ;
18+ import { FOLDER_META_FILE , joinMergeWhitespace , normalizeWhitespace , SPACE } from './utils.js' ;
1919
2020const WL_REGEXP = / ^ W L : ( \d { 1 , 2 } (?: \. \d { 1 , 2 } ) ? ) [ h H ] \s / ;
2121
22- const isListNodeWorklog = ( node : ListItem ) : boolean => {
23- const paragraph = node . children [ 0 ] ;
24- if ( ! paragraph || paragraph . type !== 'paragraph' ) {
25- return false ;
22+ const collectTextDepthFirst = ( root : Node | undefined , acc : string = '' ) : string => {
23+ if ( ! root ) {
24+ return acc ;
2625 }
27- const worklog = paragraph . children [ 0 ] ;
28- if ( ! worklog || worklog . type !== 'text' ) {
29- return false ;
26+ if ( root . type === 'text' ) {
27+ return joinMergeWhitespace ( acc , normalizeWhitespace ( ( root as Text ) . value ) ) ;
28+ }
29+ if ( 'children' in root ) {
30+ return joinMergeWhitespace ( acc , ( root as Parent ) . children . map ( child => collectTextDepthFirst ( child , acc ) ) . join ( SPACE ) ) ;
3031 }
31- return WL_REGEXP . test ( worklog . value ) ;
32+ return acc ;
3233} ;
3334
34- const trimTextNodeText = ( text : string ) => {
35- return text . trim ( )
36- . replaceAll ( / \r ? \n / g, ' ' )
37- . replace ( / \s + / , ' ' ) ;
38- } ;
39-
40- const parseTextNode = ( node : Text , ctx : ParseFileContext , curr_task : Task | null , curr_wlog : Worklog | null ) => {
41- if ( curr_wlog ) {
42- let match ;
43- if ( ! ( 'text' in curr_wlog . internal_tags ) && ( match = node . value . match ( WL_REGEXP ) ) ) {
44- const [ full , hours ] = match ;
45- const text = trimTextNodeText ( node . value . slice ( full . length ) )
46- curr_wlog . internal_tags . hours = hours ;
47- curr_wlog . internal_tags . text = text ;
48- extractTagsFromText ( text , curr_wlog . tags ) ;
49- } else {
50- extractTagsFromText ( node . value , curr_wlog . tags ) ;
51- }
52- }
53- if ( curr_task ) {
54- if ( ! ( 'text' in curr_task . internal_tags ) ) {
55- const text = trimTextNodeText ( node . value ) ;
56- curr_task . internal_tags . text = text ;
57- extractTagsFromText ( text , curr_task . tags ) ;
58- } else {
59- extractTagsFromText ( node . value , curr_task . tags ) ;
35+ const parseListItemNode = ( node : ListItem , ctx : ParseFileContext , item : Task | Worklog | null ) => {
36+ if ( ! item ) {
37+ const text = collectTextDepthFirst ( node ) ;
38+ if ( typeof node . checked === 'boolean' ) {
39+ const tags : TagMap = {
40+ ...ctx . tags ,
41+ ...ctx . heading ?. tags ,
42+ line : String ( node . position ! . start . line ) ,
43+ checked : String ( node . checked ) ,
44+ } ;
45+ tags . text = extractTagsFromText ( text , tags ) ;
46+ Object . assign ( tags , ctx . internal_tags ) ;
47+ ctx . tasks . add ( { type : 'task' , tags, file : ctx . file , worklogs : [ ] } ) ;
48+ return ;
49+ }
50+ const wl_match = text . match ( WL_REGEXP ) ;
51+ if ( wl_match ) {
52+ const [ full , hours ] = wl_match ;
53+ const tags : TagMap = {
54+ ...ctx . tags ,
55+ ...ctx . heading ?. tags ,
56+ hours,
57+ line : String ( node . position ! . start . line ) ,
58+ } ;
59+ tags . text = extractTagsFromText ( text . slice ( full . length ) , tags ) ;
60+ Object . assign ( tags , ctx . internal_tags ) ;
61+ ctx . worklogs . add ( { type : 'wlog' , tags, file : ctx . file , task : item } ) ;
62+ return ;
6063 }
6164 }
65+ parseParentNode ( node , ctx , item ) ;
6266} ;
6367
64-
65- const parseListItemNode = ( node : ListItem , ctx : ParseFileContext , curr_task : Task | null , curr_wlog : Worklog | null ) => {
66- if ( ! curr_task && typeof node . checked === 'boolean' ) {
67- const tags : TagMap = { ...ctx . tags } ;
68- const internal_tags : InternalTagMap = {
69- ...ctx . internal_tags ,
70- ...ctx . curr_heading ?. tags ,
71- line : String ( node . position ! . start . line ) ,
72- checked : String ( node . checked ) ,
73- } ;
74- const task : Task = { tags, internal_tags, file : ctx . file , worklogs : [ ] } ;
75- parseParentNode ( node as Parent , ctx , task , curr_wlog ) ;
76- Object . assign ( tags , internal_tags ) ;
77- ctx . tasks . add ( task ) ;
78- } else if ( ! curr_wlog && isListNodeWorklog ( node ) ) {
79- const tags : TagMap = { ...ctx . tags } ;
80- const internal_tags : TagMap = {
81- ...ctx . internal_tags ,
82- ...ctx . curr_heading ?. tags ,
83- line : String ( node . position ! . start . line ) ,
84- } ;
85- const worklog : Worklog = { tags, internal_tags, file : ctx . file , task : curr_task } ;
86- parseParentNode ( node as Parent , ctx , curr_task , worklog ) ;
87- Object . assign ( tags , internal_tags ) ;
88- ctx . worklogs . add ( worklog ) ;
89- } else {
90- parseParentNode ( node , ctx , curr_task , curr_wlog ) ;
91- }
92- } ;
93-
94- const parseParentNode = ( node : Parent , ctx : ParseFileContext , curr_task : Task | null , curr_wlog : Worklog | null ) => {
68+ const parseParentNode = ( node : Parent , ctx : ParseFileContext , item : Task | Worklog | null ) => {
9569 node . children . forEach ( ( node ) => {
96- parseNode ( node , ctx , curr_task , curr_wlog ) ;
70+ parseNode ( node , ctx , item ) ;
9771 } ) ;
9872} ;
9973
100- const parseHeadingNode = ( node : Heading , ctx : ParseFileContext , curr_task : Task | null , curr_wlog : Worklog | null ) => {
101- let parent = ctx . curr_heading ;
74+ const parseHeadingNode = ( node : Heading , ctx : ParseFileContext , item : Task | Worklog | null ) => {
75+ let parent = ctx . heading ;
10276 while ( parent && parent . depth > node . depth ) {
10377 parent = parent . parent ;
10478 }
10579 const tags = parent ? { ...parent . tags } : { } ;
106- const text = trimTextNodeText ( ( node . children [ 0 ] as Text ) . value ) ;
80+ const text = collectTextDepthFirst ( node ) ;
10781 extractTagsFromText ( text , tags ) ;
108- ctx . curr_heading = { depth : node . depth , tags, parent } ;
82+ ctx . heading = { depth : node . depth , tags, parent } ;
10983} ;
11084
111- const parseNode = ( node : Node , ctx : ParseFileContext , curr_task : Task | null , curr_wlog : Worklog | null ) => {
85+ const parseYamlNode = ( node : Yaml , ctx : ParseFileContext , item : Task | Worklog | null ) => {
86+ try {
87+ extractTagsFromYaml ( ( node as Yaml ) . value , ctx . tags ) ;
88+ } catch ( err ) {
89+ throw new Error ( `could not parse YAML front-matter in file ${ ctx . file } : ${ ( err as Error ) . message } ` ) ;
90+ }
91+ } ;
92+
93+ const parseNode = ( node : Node , ctx : ParseFileContext , item : Task | Worklog | null ) => {
11294 switch ( node . type ) {
11395 case 'yaml' :
114- extractTagsFromYaml ( ( node as Yaml ) . value , ctx . tags ) ;
96+ parseYamlNode ( node as Yaml , ctx , item ) ;
11597 break ;
11698 case 'listItem' :
117- parseListItemNode ( node as ListItem , ctx , curr_task , curr_wlog ) ;
118- break ;
119- case 'text' :
120- parseTextNode ( node as Text , ctx , curr_task , curr_wlog ) ;
99+ parseListItemNode ( node as ListItem , ctx , item ) ;
121100 break ;
122101 case 'heading' :
123- parseHeadingNode ( node as Heading , ctx , curr_task , curr_wlog ) ;
102+ parseHeadingNode ( node as Heading , ctx , item ) ;
124103 break ;
125104 default :
126105 if ( 'children' in node ) {
127- parseParentNode ( node as Parent , ctx , curr_task , curr_wlog ) ;
106+ parseParentNode ( node as Parent , ctx , item ) ;
128107 }
129108 }
130109} ;
@@ -154,27 +133,31 @@ export const parseFile = async (ctx: ParseFileContext) => {
154133 if ( date_match ) {
155134 ctx . tags [ 'date' ] = date_match [ 1 ] . replaceAll ( '-' , '' ) ;
156135 }
157- parseNode ( root_node , ctx , null , null ) ;
136+ parseNode ( root_node , ctx , null ) ;
158137 } catch ( err ) {
159138 if ( ( err as any ) . code !== 'ENOENT' ) {
160139 throw err ;
161140 }
162141 }
163142} ;
164143
165- const readFolderMetadata = async ( ctx : ParseContext , dir_path : string ) : Promise < TagMap | undefined > => {
144+ const readFolderMetadata = async ( ctx : ParseContext , dir_path : string ) : Promise < { tags : TagMap , ignore : boolean } > => {
145+ const target_path = resolve ( dir_path , FOLDER_META_FILE ) ;
146+ const tags : TagMap = { } ;
166147 try {
167- const target_path = resolve ( dir_path , FOLDER_META_FILE ) ;
168- const data : any = load ( await readFile ( target_path , 'utf8' ) ) ;
148+ const data : any = load ( await readFile ( target_path , 'utf8' ) ) ;
169149 if ( typeof data . tags === 'object' && data . tags !== null ) {
170- return Object . fromEntries ( Object . entries ( data . tags ) . map ( ( [ k , v ] ) => [ k , String ( v ) ] ) ) ;
150+ Object . entries ( data . tags ) . forEach ( ( [ k , v ] ) => {
151+ tags [ k ] = String ( v ) ;
152+ } ) ;
171153 }
154+ return { tags, ignore : ! ! data . ignore } ;
172155 } catch ( err ) {
173156 if ( ( err as any ) . code !== 'ENOENT' ) {
174- throw err ;
157+ throw new Error ( `could not parse folder metadata file ${ target_path } : ${ err as Error } .message` ) ;
175158 }
159+ return { tags, ignore : false } ;
176160 }
177- return undefined ;
178161} ;
179162
180163const parseFolderHelper = async ( ctx : ParseContext , target_path : string ) => {
@@ -190,20 +173,20 @@ const parseFolderHelper = async (ctx: ParseContext, target_path: string) => {
190173 } ,
191174 } ) ;
192175 } else if ( target_stats . isDirectory ( ) ) {
193- const folder_tags = await readFolderMetadata ( ctx , target_path ) ;
194- if ( folder_tags ) {
176+ const { tags , ignore } = await readFolderMetadata ( ctx , target_path ) ;
177+ if ( ! ignore ) {
195178 ctx = {
196179 ...ctx ,
197180 tags : {
198181 ...ctx . tags ,
199- ...folder_tags ,
182+ ...tags ,
200183 } ,
201184 } ;
202- }
203- const child_names = await readdir ( target_path ) ;
204- for ( const child_name of child_names ) {
205- const child_path = resolve ( target_path , child_name ) ;
206- await parseFolderHelper ( ctx , child_path ) ;
185+ const child_names = await readdir ( target_path ) ;
186+ for ( const child_name of child_names ) {
187+ const child_path = resolve ( target_path , child_name ) ;
188+ await parseFolderHelper ( ctx , child_path ) ;
189+ }
207190 }
208191 }
209192} ;
0 commit comments