@@ -7,6 +7,7 @@ import moment from 'moment/min/moment-with-locales'
77import { clo , JSP , logDebug , logInfo , logError } from './dev'
88import { displayTitle } from './general'
99import { isTermInURL , stripAllURIsAndNoteLinks , stripDoneDateTimeMentions } from './paragraph'
10+ import { escapeRegExp } from './regexEscape'
1011import { findLongestStringInArray } from './utils'
1112
1213// import { getTime } from "date-fns";
@@ -354,6 +355,69 @@ export function getEndTimeStrFromParaContent(content: string): string {
354355 }
355356}
356357
358+ /**
359+ * Normalize a time-block segment to 24h HH:mm (e.g. "12:00PM" -> "12:00", "9:00 AM" -> "09:00").
360+ * Works for either the start or end segment of a timeblock.
361+ * @param {string } startTimeStrIn - text before or after '-' in a time block
362+ * @returns {string }
363+ */
364+ export function normalizeTimeBlockStartToHHMM ( startTimeStrIn : string ) : string {
365+ let startTimeStr = startTimeStrIn
366+ if ( startTimeStr . length > 0 && startTimeStr . length < 2 ) {
367+ startTimeStr = `0 ${startTimeStr } `
368+ } else if ( startTimeStr . length > 1 && startTimeStr [ 1 ] === ':' ) {
369+ startTimeStr = `0 ${startTimeStr } `
370+ }
371+ if ( startTimeStr . endsWith ( 'AM' ) ) {
372+ startTimeStr = startTimeStr . slice ( 0 , 5 )
373+ }
374+ if ( startTimeStr . endsWith ( 'PM' ) ) {
375+ const hour = Number ( startTimeStr . slice ( 0 , 2 ) )
376+ const adjustedHour = hour === 12 ? 12 : hour + 12
377+ startTimeStr = String ( adjustedHour ) . padStart ( 2 , '0' ) + startTimeStr . slice ( 2 , 5 )
378+ }
379+ return startTimeStr
380+ }
381+
382+ /**
383+ * Return the normalised start time (HH:mm) from a paragraph's content timeblock, or 'none'.
384+ * Copes with 'AM' and 'PM' suffixes. Not fully internationalised.
385+ * @param {{ content: string } } para - anything with a .content string (TParagraph, dashboard para, etc.)
386+ * @returns {string } e.g. '10:00', or 'none' / '(error)'
387+ */
388+ export function getStartTimeFromPara ( para : { content : string } ) : string {
389+ try {
390+ let startTimeStr = 'none '
391+ const thisTimeStr = getTimeBlockString ( para . content )
392+ if ( thisTimeStr !== '' ) {
393+ startTimeStr = normalizeTimeBlockStartToHHMM ( thisTimeStr . split ( / [ - – ~ ] / ) [ 0 ] )
394+ }
395+ return startTimeStr
396+ } catch ( error ) {
397+ logError ( 'getStartTimeFromPara' , `${ error . message } ` )
398+ return '( error ) '
399+ }
400+ }
401+
402+ /**
403+ * Return the normalised end time (HH:mm) from a paragraph's content timeblock, if present.
404+ * Copes with 'AM' and 'PM' suffixes (same normalisation as getStartTimeFromPara).
405+ * @param {{ content: string } } para - anything with a .content string (TParagraph, dashboard para, etc.)
406+ * @returns {string | void } HH:mm end time, or undefined if none / error
407+ */
408+ export function getEndTimeFromPara ( para : { content : string } ) : string | void {
409+ try {
410+ const thisTimeStr = getTimeBlockString ( para . content )
411+ if ( thisTimeStr === '' ) return
412+ const endPart = thisTimeStr . split ( / [ - – ~ ] / ) [ 1 ]
413+ if ( ! endPart ) return
414+ return normalizeTimeBlockStartToHHMM ( endPart )
415+ } catch ( error ) {
416+ logError ( 'getEndTimeFromPara' , `${ error . message } ` )
417+ return
418+ }
419+ }
420+
357421/**
358422 * Return the start time of a time block in a given paragraph, or else 'none' (which will then sort after times)
359423 * Copes with 'AM' and 'PM' suffixes. Note: Not fully internationalised (but then I don't think the rest of NP accepts non-Western numerals)
@@ -394,36 +458,41 @@ export function getStartTimeObjFromParaContent(content: string): ?{ hours: numbe
394458}
395459
396460/**
397- * Return the end time of time block in a given paragraph, or else 'none' (which will then sort after times)
461+ * Return the end time of time block in a given paragraph, or else empty NaN hours/mins when absent.
398462 * Copes with 'AM' and 'PM' suffixes. Note: Not fully internationalised (but then I don't think the rest of NP accepts non-Western numerals)
399463 * @param {string } content to process
400- * @returns {{number, number} } {hours, minutes} in 24 hour clock
464+ * @returns {{ hours: number, mins: number } } {hours, minutes} in 24 hour clock (NaN if no end time)
401465 */
402466export function getEndTimeObjFromParaContent ( content : string ) : { hours : number , mins : number } {
403467 try {
404- let endTimeStr = 'none '
405468 let hours = NaN
406469 let mins = NaN
407470 if ( content !== '' ) {
408471 const thisTimeStr = getTimeBlockString ( content )
409472 if ( thisTimeStr !== '' ) {
410- endTimeStr = thisTimeStr . split ( '-' ) [ 1 ]
473+ // Prefer ASCII hyphen; also accept en-dash / tilde separators used in some timeblocks
474+ const endTimeStr = thisTimeStr . split ( / [ - – ~ ] / ) [ 1 ]
475+ if ( ! endTimeStr ) {
476+ return { hours : NaN , mins : NaN }
477+ }
478+ // Note: split with RE_AMPM_OPT as a string (same as getStartTimeObjFromParaContent) - not as RegExp,
479+ // because the optional group would match empty strings and shred the time.
411480 const [ timeStr , ampm ] = endTimeStr . split ( RE_AMPM_OPT )
412481 if ( timeStr . includes ( ':' ) ) {
413482 ; [ hours , mins ] = timeStr . split ( ':' ) . map ( Number )
414483 } else {
415484 hours = Number ( timeStr )
485+ mins = 0
416486 }
417- if ( ampm . toLowerCase ( ) === 'pm' && hours !== 12 ) {
487+ if ( ampm && ampm . toLowerCase ( ) === 'pm' && hours !== 12 ) {
418488 hours += 12
419- } else if ( ampm . toLowerCase ( ) === 'am' && hours === 12 ) {
489+ } else if ( ampm && ampm . toLowerCase ( ) === 'am' && hours === 12 ) {
420490 hours = 0
421491 }
422492 logDebug ( 'getEndTimeObjFromParaContent' , `timeStr = ${ endTimeStr } from timeblock ${ thisTimeStr } ` )
423493 }
424494 }
425- const startTime = { hours : hours , mins : mins }
426- return startTime
495+ return { hours : hours , mins : mins }
427496 } catch ( error ) {
428497 logError ( 'getEndTimeObjFromParaContent' , `${ JSP ( error ) } ` )
429498 return { hours : NaN , mins : NaN }
@@ -525,3 +594,65 @@ export function getTimeBlockDetails(content: string, timeblockTextMustContainStr
525594 return [ '' , '' ] // Return an empty tuple as a fallback
526595 }
527596}
597+
598+ /**
599+ * Strip the timeblock string and optional NotePlan "Text must contain" marker from paragraph content.
600+ * Also collapses leftover whitespace.
601+ * @param {string } content - paragraph content that may include a timeblock
602+ * @param {string } timeblockMustContainString - NotePlan preference string that may be empty
603+ * @returns {string } content with timeblock (and must-contain marker) removed
604+ */
605+ export function stripTimeBlockAndMustContainFromContent ( content : string , timeblockMustContainString : string = '' ) : string {
606+ try {
607+ if ( ! content ) return ''
608+ const timeBlockString = getTimeBlockString ( content )
609+ let cleaned = content
610+ if ( timeBlockString !== '' ) {
611+ // Remove the timeblock itself
612+ cleaned = cleaned . replace ( timeBlockString , '' )
613+ }
614+ // then the must-contain string
615+ if ( timeblockMustContainString !== '' ) {
616+ cleaned = cleaned . replace ( timeblockMustContainString , '' )
617+ }
618+ return cleaned . replace ( / \s { 2 , } / g, ' ' ) . trim ( )
619+ } catch ( err ) {
620+ logError ( 'stripTimeBlockAndMustContainFromContent' , err . message )
621+ return content ?? ''
622+ }
623+ }
624+
625+ /**
626+ * Build a display time label from dashboard para startTime/endTime (already normalised HH:mm).
627+ * @param {?string } startTime - e.g. '10:00', or 'none'/empty if absent
628+ * @param {?string } endTime - e.g. '16:30', or absent when there is no end
629+ * @returns {string } e.g. '10:00-16:30', '10:00', or ''
630+ */
631+ export function formatTimeBlockLabelFromStartAndEnd ( startTime : ?string , endTime : ?string ) : string {
632+ if ( ! startTime || startTime === 'none' ) return ''
633+ if ( endTime && endTime !== 'none' ) {
634+ return `${ startTime } -${ endTime } `
635+ }
636+ return startTime
637+ }
638+
639+ /**
640+ * Prepare timeblock content for display with the time always at the start.
641+ * Strips the original timeblock and "Text must contain" marker from content, and builds a leading
642+ * time label from the para's startTime/endTime fields.
643+ * @param {string } content - original paragraph content
644+ * @param {?string } startTime - normalised start from TParagraphForDashboard
645+ * @param {?string } endTime - normalised end from TParagraphForDashboard (optional)
646+ * @param {string } timeblockMustContainString - NotePlan preference string that may be empty
647+ * @returns {{ timeLabel: string, restContent: string } }
648+ */
649+ export function prepareTimeBlockContentForDisplay (
650+ content : string ,
651+ startTime : ?string ,
652+ endTime : ?string ,
653+ timeblockMustContainString : string = '' ,
654+ ) : { timeLabel: string , restContent : string } {
655+ const timeLabel = formatTimeBlockLabelFromStartAndEnd ( startTime , endTime )
656+ const restContent = stripTimeBlockAndMustContainFromContent ( content , timeblockMustContainString )
657+ return { timeLabel, restContent }
658+ }
0 commit comments