@@ -29,6 +29,8 @@ import fs from 'node:fs';
2929import path from 'node:path' ;
3030import { fileURLToPath } from 'node:url' ;
3131
32+ import { decodeHtmlEntities } from './html-utils.js' ;
33+
3234// ---------------------------------------------------------------------------
3335// Types
3436// ---------------------------------------------------------------------------
@@ -387,10 +389,10 @@ export function parseRiksdagKalendariumHtml(html: string): CalendarEvent[] {
387389
388390 // If no articles found, try <li> blocks (Pattern B).
389391 if ( events . length === 0 ) {
390- const liRe = / < l i \b ( [ ^ > ] * c l a s s = " [ ^ " ] * c a l e n d a r [ ^ " ] * " [ ^ > ] * ) > ( [ \s \S ] * ?) < \/ l i > / gi;
392+ const liRe = / < l i \b ( [ ^ > ] * c l a s s = ( [ " ' ] ) [ ^ " ' ] * c a l e n d a r [ ^ " ' ] * \2 [ ^ > ] * ) > ( [ \s \S ] * ?) < \/ l i > / gi;
391393 for ( const liMatch of html . matchAll ( liRe ) ) {
392394 const attrs = liMatch [ 1 ] ?? '' ;
393- const body = liMatch [ 2 ] ?? '' ;
395+ const body = liMatch [ 3 ] ?? '' ;
394396 const event = parseCalendarListItem ( attrs , body ) ;
395397 if ( event ) events . push ( event ) ;
396398 }
@@ -426,9 +428,9 @@ export function parseCalendarArticle(attrs: string, body: string): CalendarEvent
426428
427429 return {
428430 dtstart,
429- org : normalizeOrgCode ( org ) ,
430- akt : normalizeAkt ( akt ) ,
431- summary : stripTags ( summary ) . trim ( ) ,
431+ org : normalizeOrgCode ( decodeHtmlEntities ( org ) ) ,
432+ akt : normalizeAkt ( decodeHtmlEntities ( akt ) ) ,
433+ summary : decodeHtmlEntities ( stripTags ( summary ) . trim ( ) ) ,
432434 doc_refs : docRefs ,
433435 source : 'web-fallback' ,
434436 } ;
@@ -456,9 +458,9 @@ export function parseCalendarListItem(attrs: string, body: string): CalendarEven
456458
457459 return {
458460 dtstart,
459- org : normalizeOrgCode ( org ) ,
460- akt : normalizeAkt ( akt ) ,
461- summary : stripTags ( summary ) . trim ( ) ,
461+ org : normalizeOrgCode ( decodeHtmlEntities ( org ) ) ,
462+ akt : normalizeAkt ( decodeHtmlEntities ( akt ) ) ,
463+ summary : decodeHtmlEntities ( stripTags ( summary ) . trim ( ) ) ,
462464 doc_refs : docRefs ,
463465 source : 'web-fallback' ,
464466 } ;
@@ -470,15 +472,15 @@ export function parseCalendarListItem(attrs: string, body: string): CalendarEven
470472
471473/** Extract the `datetime` attribute from a `<time>` element. */
472474function extractDatetime ( html : string ) : string | null {
473- const m = html . match ( / < t i m e \b [ ^ > ] * \b d a t e t i m e = " ( [ ^ " ] + ) " / i) ;
474- return m ? ( m [ 1 ] ?? null ) : null ;
475+ const m = html . match ( / < t i m e \b [ ^ > ] * \b d a t e t i m e = ( [ " ' ] ) ( . * ? ) \1 / i) ;
476+ return m ? ( m [ 2 ] ?? null ) : null ;
475477}
476478
477479/** Extract a `data-{attr}` attribute value from a tag's attribute string. */
478480function extractDataAttr ( attrs : string , name : string ) : string | null {
479- const re = new RegExp ( `\\bdata-${ name } ="([^"]*)" ` , 'i' ) ;
481+ const re = new RegExp ( `\\bdata-${ name } \\s*=\\s*(["'])(.*?)\\1 ` , 'i' ) ;
480482 const m = attrs . match ( re ) ;
481- return m && m [ 1 ] ?. trim ( ) ? m [ 1 ] . trim ( ) : null ;
483+ return m && m [ 2 ] ?. trim ( ) ? m [ 2 ] . trim ( ) : null ;
482484}
483485
484486/** True when an element attribute string contains a `calendar-item` class token. */
@@ -492,9 +494,12 @@ function hasCalendarItemClass(attrs: string): boolean {
492494 * Uses a simple, non-greedy regex that covers the common markup pattern.
493495 */
494496function extractSpanText ( html : string , name : string ) : string | null {
495- const re = new RegExp ( `<span\\b[^>]*class="[^"]*${ name } [^"]*"[^>]*>([\\s\\S]*?)<\\/span>` , 'i' ) ;
497+ const re = new RegExp (
498+ `<span\\b[^>]*\\bclass\\s*=\\s*(["'])[^"']*${ name } [^"']*\\1[^>]*>([\\s\\S]*?)<\\/span>` ,
499+ 'i' ,
500+ ) ;
496501 const m = html . match ( re ) ;
497- return m ? stripTags ( m [ 1 ] ?? '' ) . trim ( ) || null : null ;
502+ return m ? stripTags ( m [ 2 ] ?? '' ) . trim ( ) || null : null ;
498503}
499504
500505/**
@@ -509,9 +514,9 @@ function extractHeadingAndLinks(html: string): { summary: string; docRefs: strin
509514
510515 // Collect document reference links (/sv/dokument-och-lagar/… or /dokument/…).
511516 const docRefs : string [ ] = [ ] ;
512- const hrefRe = / < a \b [ ^ > ] * \b h r e f = " ( [ ^ " ] + ) " [ ^ > ] * > / gi;
517+ const hrefRe = / < a \b [ ^ > ] * \b h r e f = ( [ " ' ] ) ( [ ^ " ' ] + ) \1 [ ^ > ] * > / gi;
513518 for ( const m of html . matchAll ( hrefRe ) ) {
514- const href = ( m [ 1 ] ?? '' ) . trim ( ) ;
519+ const href = ( m [ 2 ] ?? '' ) . trim ( ) ;
515520 if ( isRiksdagDocumentHref ( href ) ) {
516521 docRefs . push ( href ) ;
517522 }
@@ -731,9 +736,13 @@ const CALENDAR_DIR = path.join(REPO_ROOT, 'data', 'calendar');
731736 * The file is an object with `{ manifest, events }` so that consumers can
732737 * load a single file and get both the data and the provenance record.
733738 */
734- export function persistCalendarJson ( from : string , result : CalendarFetchResult ) : string {
735- fs . mkdirSync ( CALENDAR_DIR , { recursive : true } ) ;
736- const outputPath = path . join ( CALENDAR_DIR , `${ from } .json` ) ;
739+ export function persistCalendarJson (
740+ from : string ,
741+ result : CalendarFetchResult ,
742+ outputDir : string = CALENDAR_DIR ,
743+ ) : string {
744+ fs . mkdirSync ( outputDir , { recursive : true } ) ;
745+ const outputPath = path . join ( outputDir , `${ from } .json` ) ;
737746 const payload = {
738747 schema : 'riksdagsmonitor-calendar/1.0' ,
739748 manifest : result . manifest ,
0 commit comments