Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0ccb13a
Remove the <details> and <summary> parts, to have just one list.
jgclark Feb 22, 2026
23eab84
Allow project tags to be shown in col2 or col3 in table display
jgclark Feb 23, 2026
9a61a10
Remove single-section header
jgclark Feb 23, 2026
9e9bc35
Tweak to window/custom IDs in showHTMLV2() identified by Cursor
jgclark Feb 24, 2026
7432a64
b2 - add basic demo mode
jgclark Feb 24, 2026
288bae1
b3 refactoring reviews.js
jgclark Feb 25, 2026
55f790f
TaskAutomations 3.1.1 - fix weeks calculation to honor NP week
dwertheimer Feb 23, 2026
8e4e5cc
Make new endOfPreambleSection() helper by taking preamble detection o…
jgclark Feb 26, 2026
c027b79
Merge P+R v1.3.1 into 1.4 work branch
jgclark Feb 26, 2026
ceabfa5
b4: Add automatic refresh + setting autoUpdateAfterIdleTime
jgclark Feb 27, 2026
dc8f887
b5 roject metadata can now be fully stored in frontmatter as well as …
jgclark Mar 4, 2026
2a8fc8f
Merge remote-tracking branch 'origin/main' into projects-new-layout
jgclark Mar 11, 2026
bd07839
generateProjectsWeeklyProgressLines() now uses full folder paths cons…
jgclark Mar 12, 2026
b343605
b6 weekly project progress heatmaps
jgclark Mar 12, 2026
a463603
b7 improvements to lozenges + some leftovers from b6.
jgclark Mar 13, 2026
af8dca7
Remove webfonts from build as no longer necessary.
jgclark Mar 13, 2026
bbbd61c
b7 mostly lozenge UI
jgclark Mar 15, 2026
a384a20
b8 metadata improvements for FM and body of notes
jgclark Mar 16, 2026
9a265a2
b8 fix some issues on migration of metadata from body to frontmatter
jgclark Mar 18, 2026
d4b7501
b9 layout changes to use bordered project rows
jgclark Mar 20, 2026
5e99dd6
b9 Modernise layout
jgclark Mar 21, 2026
9599362
b10 add 'order by' control to top bar
jgclark Mar 21, 2026
901bf0c
Move Order control into dropdown and adjust its layout and that of to…
jgclark Mar 21, 2026
2764aeb
b11 - update changelog for first public beta of v2.0
jgclark Mar 21, 2026
a8fa3d7
improve multi-column layout
jgclark Mar 22, 2026
c317aae
b12 WIP remove 2 experimental config items about layout
jgclark Mar 22, 2026
5257226
WIP documentation updates for v2
jgclark Mar 22, 2026
59ef99d
b12 streamline CSS definitions and function names
jgclark Mar 23, 2026
0874489
Improve case insensitivity of updateFrontMatterVars()
jgclark Mar 26, 2026
5c782ea
b13 improvements to Frontmatter handling
jgclark Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions helpers/HTMLView.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import showdown from 'showdown' // for Markdown -> HTML from https://github.com/
import { hasFrontMatter } from '@helpers/NPFrontMatter'
import { getFolderFromFilename } from '@helpers/folders'
import { clo, logDebug, logError, logInfo, logWarn, JSP, timer } from '@helpers/dev'
import { getStoredWindowRect, getWindowFromCustomId, isHTMLWindowOpen, storeWindowRect } from '@helpers/NPWindows'
import { getStoredWindowRect, getWindowFromCustomId, getWindowIdFromCustomId, isHTMLWindowOpen, storeWindowRect } from '@helpers/NPWindows'
import { generateCSSFromTheme, RGBColourConvert } from '@helpers/NPThemeToCSS'
import { isTermInEventLinkHiddenPart, isTermInNotelinkOrURI, isTermInMarkdownPath } from '@helpers/paragraph'
import { RE_EVENT_LINK, RE_SYNC_MARKER, formRegExForUsersOpenTasks } from '@helpers/regex'
Expand Down Expand Up @@ -716,7 +716,10 @@ export async function sendToHTMLWindow(windowId: string, actionType: string, dat

const windowExists = isHTMLWindowOpen(windowId)
if (!windowExists) logWarn(`sendToHTMLWindow`, `Window ${windowId} does not exist; setting NPWindowID = undefined`)
const windowIdToSend = windowExists ? windowId : undefined // for iphone/ipad you have to send undefined
// runJavaScript expects the window's internal id; resolve customId to actual id when present
// TEST: this change identified by Cursor
// TEST: Not sure the comment about iphone/ipad is still relevant, but leaving it in for now.
const windowIdToSend = windowExists ? (getWindowIdFromCustomId(windowId) || windowId) : undefined // for iphone/ipad you have to send undefined

const dataWithUpdated = {
...data,
Expand Down
56 changes: 42 additions & 14 deletions helpers/NPFrontMatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ export function endOfFrontmatterLineIndex(note: CoreNoteFields): number {
try {
const paras = note.paragraphs
const lineCount = paras.length
logDebug(`paragraph/endOfFrontmatterLineIndex`, `total paragraphs in note (lineCount) = ${lineCount}`)
// logDebug(`paragraph/endOfFrontmatterLineIndex`, `total paragraphs in note (lineCount) = ${lineCount}`)
// Can't have frontmatter as less than 2 separators
if (paras.filter((p) => p.type === 'separator').length < 2) {
return 0
Expand All @@ -590,7 +590,7 @@ export function endOfFrontmatterLineIndex(note: CoreNoteFields): number {
while (lineIndex < lineCount) {
const p = paras[lineIndex]
if (p.type === 'separator') {
logDebug(`paragraph/endOfFrontmatterLineIndex`, `-> line ${lineIndex} of ${lineCount}`)
// logDebug(`paragraph/endOfFrontmatterLineIndex`, `-> line ${lineIndex} of ${lineCount}`)
return lineIndex
}
lineIndex++
Expand Down Expand Up @@ -983,8 +983,12 @@ export function determineAttributeChanges(
* @param {string} value - The attribute value to normalize.
* @returns {string} - The normalized value.
*/
export function normalizeValue(value: string): string {
return value.replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1')
export function normalizeValue(value: mixed): string {
if (value == null) {
return ''
}
const asString = typeof value === 'string' ? value : String(value)
return asString.replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1')
}

/**
Expand All @@ -1010,13 +1014,35 @@ export function updateFrontMatterVars(note: TEditor | TNote, newAttributes: { [s
logDebug('updateFrontMatterVars', `updateFrontMatterVars: note has ${note.paragraphs.length} paragraphs after ensureFrontmatter`)

const existingAttributes = { ...getFrontmatterAttributes(note) } || {}
const existingKeyByLowercase: { [string]: string } = {}
// Build lookup from raw frontmatter lines to preserve original key casing.
const existingFrontmatterParas = getFrontmatterParagraphs(note, false)
if (existingFrontmatterParas && existingFrontmatterParas.length > 0) {
existingFrontmatterParas.forEach((para) => {
const colonIndex = para.content.indexOf(':')
if (colonIndex > 0) {
const rawKey = para.content.slice(0, colonIndex).trim()
if (rawKey !== '') {
existingKeyByLowercase[rawKey.toLowerCase()] = rawKey
}
}
})
}
// Fallback to parsed attributes map when needed.
Object.keys(existingAttributes).forEach((existingKey) => {
const lcKey = existingKey.toLowerCase()
if (!existingKeyByLowercase[lcKey]) {
existingKeyByLowercase[lcKey] = existingKey
}
})
// Normalize newAttributes before comparison
clo(existingAttributes, `updateFrontMatterVars: existingAttributes`)
const normalizedNewAttributes: { [string]: any } = {}
clo(Object.keys(newAttributes), `updateFrontMatterVars: Object.keys(newAttributes) = ${JSON.stringify(Object.keys(newAttributes))}`)
Object.keys(newAttributes).forEach((key: string) => {
const value = newAttributes[key]
logDebug('updateFrontMatterVars', `newAttributes key: ${key}, value: ${value}`) // ✅
Object.keys(newAttributes).forEach((rawKey: string) => {
const canonicalKey = existingKeyByLowercase[rawKey.toLowerCase()] || rawKey
const value = newAttributes[rawKey]
logDebug('updateFrontMatterVars', `newAttributes key: ${rawKey}, value: ${value}`) // ✅

// Handle null/undefined - skip them (they won't be in normalizedNewAttributes,
// so if deleteMissingAttributes is true, they will be deleted)
Expand All @@ -1027,18 +1053,18 @@ export function updateFrontMatterVars(note: TEditor | TNote, newAttributes: { [s
let normalizedValue: string
if (typeof value === 'object') {
normalizedValue = JSON.stringify(value)
} else if (key === 'triggers') {
} else if (canonicalKey.toLowerCase() === 'triggers') {
normalizedValue = value.trim()
} else {
const trimmedValue = value.trim()
// Empty strings are allowed - they will be written as empty (not quoted)
// quoteTextIfNeededForFM will handle empty strings correctly (returns '' without quotes)
normalizedValue = quoteTextIfNeededForFM(trimmedValue)
}
logDebug('updateFrontMatterVars', `normalizedValue for key: ${key} = ${normalizedValue}`)
logDebug('updateFrontMatterVars', `normalizedValue for key: ${canonicalKey} = ${normalizedValue}`)

// $FlowIgnore
normalizedNewAttributes[key] = normalizedValue
normalizedNewAttributes[canonicalKey] = normalizedValue
})

const { keysToAdd, keysToUpdate, keysToDelete } = determineAttributeChanges(existingAttributes, normalizedNewAttributes, deleteMissingAttributes)
Expand All @@ -1062,7 +1088,8 @@ export function updateFrontMatterVars(note: TEditor | TNote, newAttributes: { [s
keysToUpdate.forEach((key: string) => {
// $FlowIgnore
const attributeLine = `${key}: ${normalizedNewAttributes[key]}`
const paragraph = note.paragraphs.find((para) => para.content.startsWith(`${key}:`))
const keyPrefixRe = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:`, 'i')
const paragraph = note.paragraphs.find((para) => keyPrefixRe.test(para.content))
if (paragraph) {
logDebug('updateFrontMatterVars', `updateFrontMatterVars: updating paragraph "${paragraph.content}" with "${attributeLine}"`)
paragraph.content = attributeLine
Expand Down Expand Up @@ -1104,18 +1131,19 @@ export function updateFrontMatterVars(note: TEditor | TNote, newAttributes: { [s
return false
}
} else {
throw new Error(`Failed to find closing '---' in note "${note.filename || ''}" could not add new attribute "${key}".`)
logError('updateFrontMatterVars', `Failed to find closing '---' in note "${note.filename || ''}" for new attribute "${key}".`)
}
})

// Delete attributes that are no longer present
const paragraphsToDelete = []
keysToDelete.forEach((key) => {
const paragraph = note.paragraphs.find((para) => para.content.startsWith(`${key}:`))
const keyPrefixRe = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:`, 'i')
const paragraph = note.paragraphs.find((para) => keyPrefixRe.test(para.content))
if (paragraph) {
paragraphsToDelete.push(paragraph)
} else {
throw new Error(`Failed to find paragraph for key "${key}".`)
logWarn('updateFrontMatterVars', `Couldn't find paragraph for key "${key}" while deleting; will continue.`)
}
})
if (paragraphsToDelete.length > 0) {
Expand Down
11 changes: 7 additions & 4 deletions helpers/NPThemeToCSS.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ function loadThemeData(themeNameIn: string = ''): { themeName: string, themeJSON
themeName = themeNameIn
logDebug('loadThemeData', `Reading theme '${themeName}'`)
themeJSON = matchingThemeObjs[0].values
currentThemeMode = Editor.currentTheme.mode
currentThemeMode = themeJSON?.mode ?? 'light'
logDebug('loadThemeData', `-> mode '${currentThemeMode}'`)
} else {
logWarn('loadThemeData', `Theme '${themeNameIn}' is not in list of available themes. Will try to use current theme instead.`)
}
Expand All @@ -51,6 +52,7 @@ function loadThemeData(themeNameIn: string = ''): { themeName: string, themeJSON
if (themeName !== '') {
themeJSON = Editor.currentTheme.values
currentThemeMode = Editor.currentTheme.mode
logDebug('loadThemeData', `-> mode '${currentThemeMode}'`)
} else {
logWarn('loadThemeData', `Cannot get settings for your current theme '${themeName}'`)
}
Expand All @@ -65,6 +67,7 @@ function loadThemeData(themeNameIn: string = ''): { themeName: string, themeJSON
logDebug('loadThemeData', `Reading your dark theme '${themeName}'`)
themeJSON = matchingThemeObjs[0].values
currentThemeMode = 'dark'
logDebug('loadThemeData', `-> mode '${currentThemeMode}'`)
} else {
logWarn('loadThemeData', `Cannot get settings for your dark theme '${themeName}'`)
}
Expand Down Expand Up @@ -158,10 +161,10 @@ export function generateCSSFromTheme(themeNameIn: string = ''): string {
rootSel.push(`--fg-warn-color: color-mix(in oklch, var(--fg-main-color), orange 20%)`)
rootSel.push(`--fg-error-color: color-mix(in oklch, var(--fg-main-color), red 20%)`)
rootSel.push(`--fg-ok-color: color-mix(in oklch, var(--fg-main-color), green 20%)`)
rootSel.push(`--bg-info-color: color-mix(in oklch, var(--bg-main-color), blue 20%)`)
rootSel.push(`--bg-info-color: color-mix(in oklch, var(--bg-main-color), blue 10%)`)
rootSel.push(`--bg-warn-color: color-mix(in oklch, var(--bg-main-color), orange 20%)`)
rootSel.push(`--bg-error-color: color-mix(in oklch, var(--bg-main-color), red 20%)`)
rootSel.push(`--bg-ok-color: color-mix(in oklch, var(--bg-main-color), green 20%)`)
rootSel.push(`--bg-error-color: color-mix(in oklch, var(--bg-main-color), red 15%)`)
rootSel.push(`--bg-ok-color: color-mix(in oklch, var(--bg-main-color), green 15%)`)
rootSel.push(`--bg-disabled-color: color-mix(in oklch, var(--bg-main-color), gray 20%)`)
rootSel.push(`--fg-disabled-color: color-mix(in oklch, var(--fg-main-color), gray 20%)`)
}
Expand Down
2 changes: 1 addition & 1 deletion helpers/NPdateTime.js
Original file line number Diff line number Diff line change
Expand Up @@ -1143,7 +1143,7 @@ export function getRelativeDates(useISODailyDates: boolean = false): Array<{ rel
const relativeDates = []
const todayMom = moment()

logInfo('NPdateTime::getRelativeDates', `Starting, with DataStore: ${typeof DataStore}`)
// logInfo('NPdateTime::getRelativeDates', `Starting, with DataStore: ${typeof DataStore}`)
if (!DataStore || typeof DataStore !== 'object') {
// A further test for DataStore.calendarNoteByDateString, as that can sometimes fail even when DataStore is available
if (!DataStore.calendarNoteByDateString) {
Expand Down
43 changes: 43 additions & 0 deletions helpers/__tests__/NPFrontMatter/NPFrontMatterAttributes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,49 @@ describe(`${PLUGIN_NAME}`, () => {
expect(barParagraph).toBeDefined()
expect(barParagraph.content).toEqual('bar: newBaz')
})

test('should update existing key regardless of case and not add duplicate key', () => {
const note = new Note({
content: '---\ntitle: foo\nDue: 2026-03-01\n---\n',
paragraphs: [
{ type: 'separator', content: '---', lineIndex: 0 },
{ content: 'title: foo', lineIndex: 1 },
{ content: 'Due: 2026-03-01', lineIndex: 2 },
{ type: 'separator', content: '---', lineIndex: 3 },
],
title: 'foo',
})

const result = f.updateFrontMatterVars(note, { title: 'foo', due: '2026-03-09' })
expect(result).toEqual(true)

const dueParagraphs = note.paragraphs.filter((p) => /^due:/i.test(p.content))
expect(dueParagraphs.length).toEqual(1)
expect(dueParagraphs[0].content).toEqual('Due: 2026-03-09')
})

test('should treat lower-case incoming key as matching existing mixed-case key when deleting missing attributes', () => {
const note = new Note({
content: '---\ntitle: foo\nDue: 2026-03-01\nOld: remove_me\n---\n',
paragraphs: [
{ type: 'separator', content: '---', lineIndex: 0 },
{ content: 'title: foo', lineIndex: 1 },
{ content: 'Due: 2026-03-01', lineIndex: 2 },
{ content: 'Old: remove_me', lineIndex: 3 },
{ type: 'separator', content: '---', lineIndex: 4 },
],
title: 'foo',
})

const result = f.updateFrontMatterVars(note, { title: 'foo', due: '2026-03-09' }, true)
expect(result).toEqual(true)

const dueParagraph = note.paragraphs.find((p) => /^due:/i.test(p.content))
expect(dueParagraph).toBeDefined()
expect(dueParagraph?.content).toEqual('Due: 2026-03-09')
const oldParagraph = note.paragraphs.find((p) => /^old:/i.test(p.content))
expect(oldParagraph).toBeUndefined()
})
})
})
})
7 changes: 6 additions & 1 deletion helpers/dateTime.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,11 @@ export function convertISODateFilenameToNPDayFilename(dailyNoteFilename: string)

// Note: ? This does not work to get reliable date string from note.date for daily notes
export function toISODateString(dateObj: Date): string {
// Guard against null/invalid Date objects to avoid runtime errors
if (dateObj == null || !(dateObj instanceof Date) || isNaN(dateObj.getTime())) {
logDebug('dateTime / toISODateString', `Invalid Date object passed: ${String(dateObj)}`)
return ''
}
// logDebug('dateTime / toISODateString', `${dateObj.toISOString()} // ${toLocaleDateTimeString(dateObj)}`)
return dateObj.toISOString().slice(0, 10)
}
Expand Down Expand Up @@ -1233,7 +1238,7 @@ export function calcOffsetDate(baseDateStrIn: string, interval: string): Date |

// calc offset (Note: library functions cope with negative nums, so just always use 'add' function)
const baseDateMoment = moment(baseDateStrIn, momentDateFormat)
const newDate = unit !== 'b' ? baseDateMoment.add(num, unitForMoment) : momentBusiness(baseDateMoment).businessAdd(num).toDate()
const newDate = unit !== 'b' ? baseDateMoment.add(num, unitForMoment).toDate() : momentBusiness(baseDateMoment).businessAdd(num).toDate()

// logDebug('dateTime / cOD', `for '${baseDateStrIn}' interval ${num} / ${unitForMoment} -> ${String(newDate)}`)
return newDate
Expand Down
Loading