33/**
44 * Key FrontMatter functions:
55 * getFrontMatterAttributes() - get the front matter attributes from a note
6- * setFrontMatterVars() - set/update the front matter attributes for a note (will create frontmatter if necessary)
6+ * updateFrontMatterVars() - update the front matter attributes for a note
7+ * (deprecated) setFrontMatterVars() - set/update the front matter attributes for a note (will create frontmatter if necessary)
78 * noteHasFrontMatter() - test whether a Test whether a Note contains front matter
89 * ensureFrontMatter() - ensure that a note has front matter (will create frontmatter if necessary)
910 * addTrigger() - add a trigger to the front matter (will create frontmatter if necessary)
1011 */
1112
1213import fm from 'front-matter'
1314// import { showMessage } from './userInput'
14- import { clo , JSP , logDebug , logError , logWarn , timer } from '@helpers/dev'
15+ import { clo , clof , JSP , logDebug , logError , logWarn , timer } from '@helpers/dev'
1516import { displayTitle } from '@helpers/general'
1617import { RE_MARKDOWN_LINKS_CAPTURE_G } from '@helpers/regex'
1718const pluginJson = 'helpers/NPFrontMatter.js'
@@ -148,7 +149,7 @@ export function removeFrontMatterField(note: CoreNoteFields, fieldToRemove: stri
148149 Object . keys ( fmFields ) . forEach ( ( thisKey ) => {
149150 if ( thisKey === fieldToRemove ) {
150151 const thisValue = fmFields [ thisKey ]
151- // logDebug('rFMF', `- for thisKey ${thisKey}, looking for <${fieldToRemove}:${value ?? "<undefined> "}> to remove. thisValue=${thisValue}`)
152+ // logDebug('rFMF', `- for thisKey ${thisKey}, looking for <${fieldToRemove}:${value ?? "<undefined} " }> to remove. thisValue=${thisValue}`)
152153 if ( ! value || thisValue === value ) {
153154 // logDebug('rFMF', ` - value:${value ?? "<undefined>"} / thisValue:${value ?? "<undefined>"}`)
154155 // remove this attribute fully
@@ -230,21 +231,8 @@ export function writeFrontMatter(note: CoreNoteFields, attributes: { [string]: s
230231 return false
231232 }
232233 if ( ensureFrontmatter ( note , alsoEnsureTitle ) ) {
233- const outputArr = [ ]
234- Object . keys ( attributes ) . forEach ( ( key ) => {
235- const value = attributes [ key ]
236- if ( value !== null ) {
237- if ( typeof value === 'string' ) {
238- outputArr . push ( quoteNonStandardYaml ? `${ key } : ${ quoteText ( value ) } ` : `${ key } : ${ value } ` )
239- } else {
240- if ( typeof value === 'object' ) {
241- logDebug ( pluginJson , `writeFrontMatter: value for key '${ key } ' is an object (e.g. multi-level list). Converting to multi-line string.` )
242- const yamlString = _objectToYaml ( value )
243- outputArr . push ( `${ key } :${ yamlString } ` )
244- }
245- }
246- }
247- } )
234+ const outputArr = createFrontmatterTextArray ( attributes , quoteNonStandardYaml )
235+
248236 const output = outputArr . join ( '\n' )
249237 logDebug ( pluginJson , `writeFrontMatter: writing frontmatter to note '${ displayTitle ( note ) } ':\n"${ output } "` )
250238 note . insertParagraph ( output , 1 , 'text' )
@@ -258,6 +246,7 @@ export function writeFrontMatter(note: CoreNoteFields, attributes: { [string]: s
258246export const hasTemplateTagsInFM = ( fmText : string ) : boolean => fmText . includes ( '<%' )
259247
260248/**
249+ * NOTE: This function is deprecated. Use the more efficient updateFrontMatterVars() instead.
261250 * Set/update the front matter attributes for a note.
262251 * Whatever key:value pairs you pass in will be set in the front matter.
263252 * If the key already exists, it will be set to the new value you passed;
@@ -271,6 +260,7 @@ export const hasTemplateTagsInFM = (fmText: string): boolean => fmText.includes(
271260 */
272261export function setFrontMatterVars ( note : CoreNoteFields , varObj : { [ string ] : string } ) : boolean {
273262 try {
263+ logDebug ( pluginJson , `setFrontMatterVars: this function is deprecated. Use updateFrontMatterVars() instead.` )
274264 const title = varObj . title || null
275265 clo ( varObj , `Starting for note ${ note . filename } with varObj:` )
276266 logDebug ( `setFrontMatterVars` , `- BEFORE ensureFM: hasFrontmatter:${ String ( noteHasFrontMatter ( note ) || '' ) } note has ${ note . paragraphs . length } lines` )
@@ -280,6 +270,7 @@ export function setFrontMatterVars(note: CoreNoteFields, varObj: { [string]: str
280270 if ( ! hasFM ) {
281271 throw new Error ( `setFrontMatterVars: Could not add front matter to note which has no title. Note should have a title, or you should pass in a title in the varObj.` )
282272 }
273+
283274 if ( hasFrontMatter ( note . content || '' ) ) {
284275 const existingAttributes = getAttributes ( note . content )
285276 const changedAttributes = { ...existingAttributes }
@@ -297,6 +288,7 @@ export function setFrontMatterVars(note: CoreNoteFields, varObj: { [string]: str
297288 } else {
298289 logError ( 'setFrontMatterVars' , `- could not change frontmatter for note "${ note . filename || '' } " because it has no frontmatter.` )
299290 }
291+
300292 return true
301293 } catch ( error ) {
302294 logError ( 'NPFrontMatter/setFrontMatterVars()' , JSP ( error ) )
@@ -343,7 +335,6 @@ export function ensureFrontmatter(note: CoreNoteFields, alsoEnsureTitle: boolean
343335 } else if ( hasFrontMatter ( note . content || '' ) ) {
344336 // already has frontmatter
345337 const attr = getAttributes ( note . content )
346- clo ( attr , `ensureFrontmatter: Note '${ displayTitle ( note ) } ' has frontmatter already: attr =` )
347338 if ( ! attr . title && title ) {
348339 logDebug ( 'ensureFrontmatter' , `Note '${ displayTitle ( note ) } ' already has frontmatter but no title. Adding title.` )
349340 if ( note . content ) note . content = note . content . replace ( '---' , `---\ntitle: ${ title } \n` )
@@ -706,12 +697,12 @@ export function getBody(templateData: string = ''): string {
706697 * @usage if (Editor?.note && isTriggerLoop(Editor.note)) return // returns/stopping execution if the time since the last document write is less than than 2000ms
707698 * @author @dwertheimer extended by @jgclark
708699 */
709- export function isTriggerLoop ( note : TNote , minimumTimeRequired : number = 2000 ) : boolean {
700+ export function isTriggerLoop ( note : TNote , minimumTimeRequiredMS : number = 2000 ) : boolean {
710701 try {
711- if ( ! note . versions || ! note . versions . length || note . versions [ 0 ] ) return false // no note version, so no recent update
702+ if ( ! note . versions || ! note . versions . length ) return false // no note version, so no recent update
712703
713704 const timeSinceLastEdit : number = Date . now ( ) - note . versions [ 0 ] . date
714- if ( timeSinceLastEdit <= minimumTimeRequired ) {
705+ if ( timeSinceLastEdit <= minimumTimeRequiredMS ) {
715706 logDebug ( pluginJson , `isTriggerLoop: only ${ String ( timeSinceLastEdit ) } ms after the last document write. Stopping execution to avoid infinite loop.` )
716707 return true
717708 }
@@ -721,3 +712,149 @@ export function isTriggerLoop(note: TNote, minimumTimeRequired: number = 2000):
721712 return false
722713 }
723714}
715+
716+ /**
717+ * Determine which attributes need to be added, updated, or deleted.
718+ * @param {{ [string]: string } } existingAttributes - Current front matter attributes.
719+ * @param {{ [string]: string } } newAttributes - Desired front matter attributes.
720+ * @param {boolean } deleteMissingAttributes - Whether to delete attributes that are not present in newAttributes (default: false)
721+ * @returns {{
722+ * keysToAdd: Array<string>,
723+ * keysToUpdate: Array<string>,
724+ * keysToDelete: Array<string>
725+ * }}
726+ */
727+ export function determineAttributeChanges (
728+ existingAttributes : { [ string ] : string } ,
729+ newAttributes : { [ string ] : string } ,
730+ deleteMissingAttributes : boolean = false ,
731+ ) : {
732+ keysToAdd : Array < string > ,
733+ keysToUpdate : Array < string > ,
734+ keysToDelete : Array < string > ,
735+ } {
736+ const keysToAdd = Object . keys ( newAttributes ) . filter ( ( key ) => ! ( key in existingAttributes ) )
737+ const keysToUpdate = Object . keys ( newAttributes ) . filter ( ( key ) => key in existingAttributes && normalizeValue ( existingAttributes [ key ] ) !== normalizeValue ( newAttributes [ key ] ) )
738+ const keysToDelete : Array < string > = [ ]
739+ if ( deleteMissingAttributes ) {
740+ keysToDelete . push ( ...Object . keys ( existingAttributes ) . filter ( ( key ) => key !== 'title' && ! ( key in newAttributes ) ) )
741+ }
742+ return { keysToAdd, keysToUpdate, keysToDelete }
743+ }
744+
745+ /**
746+ * Normalize attribute values by removing quotes for comparison.
747+ * @param {string } value - The attribute value to normalize.
748+ * @returns {string } - The normalized value.
749+ */
750+ export function normalizeValue ( value : string ) : string {
751+ return value . replace ( / ^ " ( .* ) " $ / , '$1' ) . replace ( / ^ ' ( .* ) ' $ / , '$1' )
752+ }
753+
754+ /**
755+ * Update existing front matter attributes based on the provided newAttributes.
756+ * Assumes that newAttributes is the complete desired set of attributes.
757+ * Adds new attributes, updates existing ones, and deletes any that are not present in newAttributes.
758+ * @param {CoreNoteFields } note - The note to update.
759+ * @param {{ [string]: string } } newAttributes - The complete set of desired front matter attributes.
760+ * @param {boolean } deleteMissingAttributes - Whether to delete attributes that are not present in newAttributes (default: false)
761+ * @returns {boolean } - Whether the front matter was updated successfully.
762+ */
763+ export function updateFrontMatterVars ( note : CoreNoteFields , newAttributes : { [ string ] : string } , deleteMissingAttributes : boolean = false ) : boolean {
764+ try {
765+ // Ensure the note has front matter
766+ if ( ! ensureFrontmatter ( note ) ) {
767+ logError ( pluginJson , `updateFrontMatterVars: Failed to ensure front matter for note "${ note . filename || '' } ".` )
768+ return false
769+ }
770+
771+ const existingAttributes = getFrontMatterAttributes ( note ) || { }
772+ // Normalize newAttributes before comparison
773+ const normalizedNewAttributes = { }
774+ Object . keys ( newAttributes ) . forEach ( ( key : string ) => {
775+ const value = newAttributes [ key ]
776+ // $FlowIgnore
777+ normalizedNewAttributes [ key ] = typeof value === 'object' ? JSON . stringify ( value ) : quoteText ( value )
778+ } )
779+
780+ const { keysToAdd , keysToUpdate , keysToDelete } = determineAttributeChanges ( existingAttributes , normalizedNewAttributes , deleteMissingAttributes )
781+
782+ keysToAdd . length > 0 && clo ( keysToAdd , `updateFrontMatterVars: keysToAdd` )
783+ keysToUpdate . length > 0 && clo ( keysToUpdate , `updateFrontMatterVars: keysToUpdate` )
784+ keysToDelete . length > 0 && clo ( keysToDelete , `updateFrontMatterVars: keysToDelete` )
785+
786+ // Update existing attributes -- just replace the text in the paragraph
787+ keysToUpdate . forEach ( ( key : string ) => {
788+ // $FlowIgnore
789+ const attributeLine = `${ key } : ${ normalizedNewAttributes [ key ] } `
790+ const paragraph = note . paragraphs . find ( ( para ) => para . content . startsWith ( `${ key } :` ) )
791+ if ( paragraph ) {
792+ logDebug ( pluginJson , `updateFrontMatterVars: updating paragraph "${ paragraph . content } " with "${ attributeLine } "` )
793+ paragraph . content = attributeLine
794+ note . updateParagraph ( paragraph )
795+ } else {
796+ logError ( pluginJson , `updateFrontMatterVars: Failed to find frontmatter paragraph for key "${ key } ".` )
797+ }
798+ } )
799+
800+ // Add new attributes to the end of the frontmatter
801+ keysToAdd . forEach ( ( key ) => {
802+ // $FlowIgnore
803+ const newAttributeLine = `${ key } : ${ normalizedNewAttributes [ key ] } `
804+ // Insert before the closing '---'
805+ const closingIndex = note . paragraphs . findIndex ( ( para ) => para . content . trim ( ) === '---' && para . lineIndex > 0 )
806+ if ( closingIndex !== - 1 ) {
807+ note . insertParagraph ( newAttributeLine , closingIndex , 'text' )
808+ } else {
809+ logError ( pluginJson , `updateFrontMatterVars: Failed to find closing '---' in note "${ note . filename || '' } " could not add new attribute "${ key } ".` )
810+ }
811+ } )
812+
813+ // Delete attributes that are no longer present
814+ const paragraphsToDelete = [ ]
815+ keysToDelete . forEach ( ( key ) => {
816+ const paragraph = note . paragraphs . find ( ( para ) => para . content . startsWith ( `${ key } :` ) )
817+ if ( paragraph ) {
818+ paragraphsToDelete . push ( paragraph )
819+ } else {
820+ logError ( pluginJson , `updateFrontMatterVars: Failed to find paragraph for key "${ key } ".` )
821+ }
822+ } )
823+ if ( paragraphsToDelete . length > 0 ) {
824+ note . removeParagraphs ( paragraphsToDelete )
825+ }
826+
827+ return true
828+ } catch ( error ) {
829+ logError ( 'NPFrontMatter/updateFrontMatterVars()' , JSP ( error ) )
830+ return false
831+ }
832+ }
833+
834+ /**
835+ * Create an array of frontmatter text from the provided attributes.
836+ * Deals with multi-level lists and values that need to be quoted (e.g. strings that contain colons, or @mentions).
837+ * @param {Object } attributes - The attributes to convert to frontmatter text.
838+ * @param {boolean } quoteNonStandardYaml - Whether to quote non-standard YAML values.
839+ * @returns {Array<string> } - An array of frontmatter text.
840+ */
841+ export function createFrontmatterTextArray ( attributes : { [ string ] : string } , quoteNonStandardYaml : boolean ) : Array < string > {
842+ const outputArr = [ ]
843+ Object . keys ( attributes ) . forEach ( ( key ) => {
844+ const value = attributes [ key ]
845+ if ( value !== null ) {
846+ if ( typeof value === 'string' ) {
847+ outputArr . push ( quoteNonStandardYaml ? `${ key } : ${ quoteText ( value ) } ` : `${ key } : ${ value } ` )
848+ } else if ( Array . isArray ( value ) ) {
849+ const arrayString = value . map ( ( item : string ) => ` - ${ item } ` ) . join ( '\n' )
850+ outputArr . push ( `${ key } :\n${ arrayString } ` )
851+ } else if ( typeof value === 'object' ) {
852+ const yamlString = _objectToYaml ( value , ' ' )
853+ outputArr . push ( `${ key } :${ yamlString } ` )
854+ } else {
855+ outputArr . push ( `${ key } : ${ value } ` )
856+ }
857+ }
858+ } )
859+ return outputArr
860+ }
0 commit comments