Skip to content

Commit bcf95b7

Browse files
committed
Merge branch 'main' of https://github.com/NotePlan/plugins
2 parents a48f41e + 5fc4e3f commit bcf95b7

34 files changed

+1933
-22176
lines changed

.cursorignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
1,
3939
{
4040
"args": "all",
41-
"varsIgnorePattern": "^_|^clo$|^timer$|^log|^JSP$|^clof$|^res$",
41+
"varsIgnorePattern": "^_|^clo$|^timer$|^log|^JSP$|^clof$|^res$|^describe$|^test$|^expect$|^beforeAll$|customConsole$|simpleFormatter$|Note$|^Paragraph$|^NotePlan$|^Editor$|^DataStore$|^CommandBar$|^Calendar$|^CalendarItem$|^Clipboard$|^HTMLView$|^globalThis$",
4242
"argsIgnorePattern": "^_"
4343
}
4444
],

.github/workflows/node.js.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Disable/enable this script at: https://github.com/NotePlan/plugins/actions/workflows/node.js.yml
2+
13
# Run tests on push or pull_request
24
# per: https://joelhooks.com/jest-and-github-actions/
35
# also uses github-actions-reporter.js for reporting

__mocks__/DataStore.mock.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*
88
*/
99
import * as samplePlugin from './support/pluginSample.json'
10-
10+
import { logDebug } from '@helpers/dev'
1111
let __json = samplePlugin //variable used for saving/getting json
1212

1313
export const DataStore = {
@@ -38,7 +38,10 @@ export const DataStore = {
3838
defaultFileExtension: 'md',
3939
/* folders: [{ return / }], */
4040
// async installOrUpdatePluginsByID() { return null },
41-
// async installPlugin() { return null },
41+
async installPlugin(pluginObject, showLoading = false) {
42+
logDebug('DataStore.installPlugin (mock)', `requested install of plugin: ${pluginObject.id}; showLoading: ${showLoading}; returning null`)
43+
return null
44+
},
4245
// async installedPlugins() { return null },
4346
// async invokePluginCommand() { return null },
4447
// async invokePluginCommandByName() { return null },

dwertheimer.ReactSkeleton/src/reactMain.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export type PassedData = {
2929
export function getInitialDataForReactWindow(): PassedData {
3030
const startTime = new Date()
3131
// get whatever pluginData you want the React window to start with and include it in the object below. This all gets passed to the React window
32-
const pluginData = getInitialDataForReactWindow()
32+
const pluginData = getPluginData()
3333
const ENV_MODE = 'development' /* helps during development. set to 'production' when ready to release */
3434
const dataToPass: PassedData = {
3535
pluginData,
@@ -52,7 +52,7 @@ export function getInitialDataForReactWindow(): PassedData {
5252
* properties: pluginData, title, debug, ENV_MODE, returnPluginCommand, componentPath, passThroughVars, startTime
5353
* @returns {[string]: mixed} - the data that your React Window will start with
5454
*/
55-
export function getInitialDataForReactWindow(): { [string]: mixed } {
55+
export function getPluginData(): { [string]: mixed } {
5656
// for demonstration purposes will just fake some data for now,
5757
// you would want to gather some data from your plugin
5858
const data = Array.from(Array(10).keys()).map((i) => ({ textValue: `Item ${i}`, id: i, buttonText: `Submit ${i}` }))

helpers/NPConfiguration.js

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -312,17 +312,19 @@ export type PluginObjectWithUpdateField = {
312312
* @returns the plugin object if the id is found and the minVersion matches (>= the minVersion)
313313
*/
314314
export const findPluginInList = (list: Array<any>, pluginID: string, minVersion?: string = '0.0.0'): any => {
315-
return list.find((p) => {
316-
if (p.id === pluginID) {
317-
logDebug(
318-
`findPluginInList: ${p.id} ${p.version} (${semverVersionToNumber(p.version)}) >= ${minVersion} ${String(
319-
minVersion ? semverVersionToNumber(p.version) >= semverVersionToNumber(minVersion) : true,
320-
)}`,
321-
)
322-
return minVersion ? semverVersionToNumber(p.version) >= semverVersionToNumber(minVersion) : true
323-
}
324-
return false
325-
})
315+
return list && Array.isArray(list)
316+
? list.find((p) => {
317+
if (p.id === pluginID) {
318+
logDebug(
319+
`findPluginInList: ${p.id} ${p.version} (${semverVersionToNumber(p.version)}) >= ${minVersion} ${String(
320+
minVersion ? semverVersionToNumber(p.version) >= semverVersionToNumber(minVersion) : true,
321+
)}`,
322+
)
323+
return minVersion ? semverVersionToNumber(p.version) >= semverVersionToNumber(minVersion) : true
324+
}
325+
return false
326+
})
327+
: null
326328
}
327329

328330
/**
@@ -342,7 +344,7 @@ export function pluginIsInstalled(pluginID: string, minVersion?: string): boolea
342344
* @returns {Array<string>} - list of command names
343345
*/
344346
export function getPluginCommandNames(pluginID: string): Array<string> {
345-
const thisPluginObj = DataStore.installedPlugins().find(p => p.id === pluginID)
347+
const thisPluginObj = DataStore.installedPlugins().find((p) => p.id === pluginID)
346348
if (!thisPluginObj) {
347349
logWarn('getPluginCommandNames', `could not find installed plugin ${pluginID}`)
348350
return []

helpers/NPFrontMatter.js

Lines changed: 159 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
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

1213
import 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'
1516
import { displayTitle } from '@helpers/general'
1617
import { RE_MARKDOWN_LINKS_CAPTURE_G } from '@helpers/regex'
1718
const 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
258246
export 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
*/
272261
export 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

Comments
 (0)