Skip to content

Commit 01aa0a7

Browse files
committed
Templating2.4.1 -- deal with separators at top and bottom of body
1 parent a14952e commit 01aa0a7

6 files changed

Lines changed: 184 additions & 26 deletions

File tree

‎helpers/NPFrontMatter.js‎

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,9 @@ export type FrontMatterDocumentObject = { attributes: { [string]: string }, body
816816
* @returns {Object} - the frontmatter object (or empty object if none)
817817
*/
818818
export function getSanitizedFmParts(noteText: string, removeTemplateTagsInFM?: boolean = false): FrontMatterDocumentObject {
819+
const doubleDashFm = tryExtractLeadingDoubleDashFm(noteText || '')
820+
if (doubleDashFm) return doubleDashFm
821+
819822
let fmData = { attributes: {}, body: noteText, frontmatter: '' } //default
820823

821824
// we need to pre-process the text to sanitize it instead of running fm because we need to
@@ -1468,6 +1471,12 @@ export async function getValuesForFrontmatterTag(
14681471

14691472
/**
14701473
* Analyze a template's structure to determine various characteristics
1474+
*
1475+
* **Output (new-note) frontmatter** may appear in `--` … `--` or `---` … `---` pairs only when the pair opens on
1476+
* the **first line of the template body** (the first line after the template’s own YAML frontmatter, if any).
1477+
* If the fenced region contains no valid YAML-like lines, it is **not** treated as frontmatter: the fences and
1478+
* everything between stay ordinary note content (e.g. horizontal rules and headings).
1479+
*
14711480
* @param {string} templateData - The template content to analyze
14721481
* @returns {Object} Analysis results with the following properties:
14731482
* - hasNewNoteTitle: boolean - Whether template has 'newNoteTitle' in frontmatter
@@ -1629,24 +1638,28 @@ export function analyzeTemplateStructure(templateData: string): {
16291638
}
16301639

16311640
/**
1632-
* Helper function to find separator positions in an array of lines
1633-
* Looks for both -- and --- separators
1634-
* @param {Array<string>} lines - Array of lines to search
1635-
* @param {number} startIndex - Index to start searching from (default: 0)
1636-
* @returns {{startIndex: number, endIndex: number}} - Object with start and end indices, or {-1, -1} if not found
1641+
* Find the closing pair for an output-frontmatter fence starting at startIndex.
1642+
* Uses the **opening line’s** delimiter: `---` pairs with the next `---` only; `--` pairs with the next `--` only.
1643+
* (Searching for `--` first broke `---` bodies that contained a standalone `--` line.)
1644+
* @param {Array<string>} lines - Lines to search
1645+
* @param {number} startIndex - Line index of the opening `--` or `---`
1646+
* @returns {{startIndex: number, endIndex: number}}
16371647
*/
16381648
function findSeparatorPositions(lines: Array<string>, startIndex: number = 0): { startIndex: number, endIndex: number } {
1639-
// First try to find -- separators
1640-
let startPos = lines.indexOf('--', startIndex)
1641-
let endPos = startPos >= 0 ? lines.indexOf('--', startPos + 1) : -1
1642-
1643-
// If no -- separators found, try to find --- separators
1644-
if (startPos === -1) {
1645-
startPos = lines.indexOf('---', startIndex)
1646-
endPos = startPos >= 0 ? lines.indexOf('---', startPos + 1) : -1
1649+
const first = (lines[startIndex] || '').trim()
1650+
if (first === '---') {
1651+
const startPos = startIndex
1652+
const rel = lines.slice(startPos + 1).findIndex((l) => l.trim() === '---')
1653+
if (rel < 0) return { startIndex: -1, endIndex: -1 }
1654+
return { startIndex: startPos, endIndex: startPos + 1 + rel }
16471655
}
1648-
1649-
return { startIndex: startPos, endIndex: endPos }
1656+
if (first === '--') {
1657+
const startPos = startIndex
1658+
const rel = lines.slice(startPos + 1).findIndex((l) => l.trim() === '--')
1659+
if (rel < 0) return { startIndex: -1, endIndex: -1 }
1660+
return { startIndex: startPos, endIndex: startPos + 1 + rel }
1661+
}
1662+
return { startIndex: -1, endIndex: -1 }
16501663
}
16511664

16521665
/**
@@ -1694,6 +1707,27 @@ function extractAndParseFrontmatter(lines: Array<string>, startIndex: number, en
16941707
return { attributes: {}, isValid: false }
16951708
}
16961709

1710+
/**
1711+
* `front-matter` does not recognize `--` fences. When the document starts with `--` … `--` and the inner region is
1712+
* YAML-like output frontmatter, parse it here so render/new-note logic matches `---` behavior.
1713+
* If the inner region is not valid YAML-like content, return null so the whole string stays body text.
1714+
* @param {string} noteText - Full template or fragment
1715+
* @returns {FrontMatterDocumentObject | null}
1716+
*/
1717+
function tryExtractLeadingDoubleDashFm(noteText: string): FrontMatterDocumentObject | null {
1718+
const lines = (noteText || '').split('\n')
1719+
if (lines.length < 2 || lines[0].trim() !== '--') return null
1720+
1721+
for (let i = 1; i < lines.length; i++) {
1722+
if (lines[i].trim() !== '--') continue
1723+
const { attributes, isValid } = extractAndParseFrontmatter(lines, 0, i)
1724+
if (!isValid) return null
1725+
const body = lines.slice(i + 1).join('\n')
1726+
return { attributes, body, frontmatter: '' }
1727+
}
1728+
return null
1729+
}
1730+
16971731
/**
16981732
* Helper function to get the folder path array from a note's filename
16991733
* @param {string} filename - The note's filename
@@ -2006,6 +2040,8 @@ export function getNoteTitleFromRenderedContent(renderedContent: string): string
20062040

20072041
/**
20082042
* Check if content between --- markers is valid YAML-like content
2043+
* Headings like `## Event:**` contain a colon and used to match the `key:` heuristic; a note body
2044+
* wrapped in `---` / `---` (horizontal rules) was then mistaken for YAML, producing an empty body after parse.
20092045
* @param {string} content - The content to validate
20102046
* @returns {boolean} - Whether the content is valid YAML-like frontmatter
20112047
*/
@@ -2022,6 +2058,9 @@ export function isValidYamlContent(content: string): boolean {
20222058
const trimmedLine = line.trim()
20232059
if (trimmedLine === '') continue // Skip empty lines
20242060

2061+
// Markdown ATX headings (# … ###### ) can include colons (e.g. "## Event:**"); do not treat as YAML.
2062+
if (/^#{1,6}\s/.test(trimmedLine)) continue
2063+
20252064
// Check for valid YAML patterns:
20262065
// 1. key: value (with optional spaces) - allows hyphens and spaces in key names
20272066
// 2. key: (empty value) - allows hyphens and spaces in key names

‎helpers/__tests__/NPFrontMatter/NPFrontMatter.analyzeTemplateStructure.test.js‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,66 @@ Some content here`
699699
})
700700
})
701701

702+
describe('output frontmatter: -- and --- only when first body line + valid YAML', () => {
703+
test('treats --- … --- at body start as content when inner is not YAML (e.g. headings / HR)', () => {
704+
const template = `---
705+
title: Template FM
706+
---
707+
---
708+
## Event:** note
709+
710+
### Agenda
711+
-
712+
---
713+
After`
714+
const result = analyzeTemplateStructure(template)
715+
716+
expect(result.hasOutputFrontmatter).toBe(false)
717+
expect(result.outputFrontmatter).toEqual({})
718+
expect(result.bodyContent).toContain('## Event:** note')
719+
expect(result.bodyContent).toContain('After')
720+
})
721+
722+
test('parses valid new-note frontmatter in -- … -- when first body line opens the fence', () => {
723+
const template = `---
724+
title: Template FM
725+
---
726+
--
727+
noteTitle: From output FM
728+
type: project
729+
--
730+
# Hello`
731+
732+
const result = analyzeTemplateStructure(template)
733+
734+
expect(result.hasOutputFrontmatter).toBe(true)
735+
expect(result.outputFrontmatter.noteTitle).toBe('From output FM')
736+
expect(result.outputFrontmatter.type).toBe('project')
737+
expect(result.bodyContent).toContain('noteTitle: From output FM')
738+
expect(result.bodyContent).toContain('# Hello')
739+
})
740+
741+
test('pairs closing --- with next --- even if body contains a standalone -- line', () => {
742+
const template = `---
743+
t: 1
744+
---
745+
---
746+
middle: kept
747+
--
748+
not-closing-triple
749+
---
750+
end body`
751+
752+
const result = analyzeTemplateStructure(template)
753+
754+
expect(result.hasOutputFrontmatter).toBe(true)
755+
expect(result.outputFrontmatter.middle).toBe('kept')
756+
expect(result.bodyContent).toContain('--')
757+
expect(result.bodyContent).toContain('not-closing-triple')
758+
expect(result.bodyContent).toContain('end body')
759+
})
760+
})
761+
702762
describe('template vs rendered content handling', () => {
703763
test('should detect inline title from template content with EJS tags (current behavior)', () => {
704764
const templateWithEJS = `---

‎helpers/__tests__/NPFrontMatter/NPFrontMatterMisc.test.js‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,34 @@ describe(`${PLUGIN_NAME}`, () => {
161161
expect(result).toBe(false)
162162
})
163163

164+
test('should return false when only YAML-looking lines are markdown headings with colons (e.g. ## Event:**)', () => {
165+
const markdownBetweenDashes = `## Event:** <%- calendarItemLink %>
166+
167+
### Agenda
168+
-
169+
### Meeting Minutes
170+
-
171+
### Action Items
172+
*`
173+
const result = f.isValidYamlContent(markdownBetweenDashes)
174+
expect(result).toBe(false)
175+
})
176+
177+
test('getSanitizedFmParts strips leading -- fence when inner is valid YAML', () => {
178+
const doc = `--\ntitle: Double Dash FM\ntype: test\n--\nRest`
179+
const result = f.getSanitizedFmParts(doc)
180+
expect(result.attributes.title).toBe('Double Dash FM')
181+
expect(result.attributes.type).toBe('test')
182+
expect(result.body).toBe('Rest')
183+
})
184+
185+
test('getSanitizedFmParts leaves full text as body when leading -- fence inner is not YAML', () => {
186+
const doc = `--\n## Nope:**\n--\nTail`
187+
const result = f.getSanitizedFmParts(doc)
188+
expect(result.attributes).toEqual({})
189+
expect(result.body).toBe(doc)
190+
})
191+
164192
test('should return true for complex real-world examples', () => {
165193
const yamlContent = `title: Meeting Note
166194
note-tag: #meeting

‎np.Templating/CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ See Plugin [Documentation](https://noteplan.co/templates/docs) for details on av
66

77
DBW: REMEMBER THAT IF YOU ADDED ANY HELPERS IMPORTS, ADD THEM TO THE HELPER MODULE TO GIVE SCRIPTS ACCESS TO THEM ALSO
88

9+
## [2.4.2] 2026-05-14 @dwertheimer
10+
11+
### Fixed
12+
13+
- **`isValidYamlContent` vs markdown headings:** Headings such as `## Event:**` include a colon and were counted as YAML key lines. A template body framed with `---` horizontal rules (start and end) was then misclassified as a frontmatter block, `parse` returned an empty body, and Meeting Notes / renders produced no output. Markdown ATX heading lines (`#` … `######` followed by whitespace) are ignored for YAML detection so `---` around normal note text is not treated as frontmatter.
14+
15+
### Changed
16+
17+
- **New-note / output frontmatter rules:** Fences at the **first line of the template body** (after the template’s own YAML frontmatter) may use `--` or `---`. The enclosed region is treated as output frontmatter **only** if it is YAML-like (`isValidYamlContent`); otherwise the fences and text between stay normal content. Closing delimiter pairs match the opener (`---` with the next `---`, not an intervening `--` line). Leading `--` blocks are parsed in `getSanitizedFmParts` because `front-matter` ignores `--` fences. `FrontmatterModule.isFrontmatterTemplate` accepts both `--` and `---` openers with the same YAML gate.
18+
19+
### Edited in this release
20+
21+
- `helpers/NPFrontMatter.js`, `helpers/__tests__/NPFrontMatter/NPFrontMatterMisc.test.js`, `helpers/__tests__/NPFrontMatter/NPFrontMatter.analyzeTemplateStructure.test.js`
22+
- `np.Templating/lib/support/modules/FrontmatterModule.js`
23+
- `np.Templating/CHANGELOG.md`, `np.Templating/plugin.json` — version **2.4.2**
24+
925
## [2.4.1] 2026-05-03 @dwertheimer
1026

1127
### Fixed

‎np.Templating/lib/support/modules/FrontmatterModule.js‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,35 @@ export default class FrontmatterModule {
3030
}
3131
}
3232

33+
/**
34+
* Whether the string begins with a `--` or `---` fence whose inner lines are YAML-like (see `isValidYamlContent`).
35+
* If the inner region is not YAML (e.g. markdown / HRs only), returns false so the text is treated as body content.
36+
* @param {string} templateData - Note or template fragment (often starts at the first line).
37+
* @returns {boolean}
38+
*/
3339
isFrontmatterTemplate(templateData: string): boolean {
34-
// First check if the template has the frontmatter structure (starts with --- and has another ---)
35-
if (!templateData.startsWith('---')) return false
36-
const lines = templateData.split('\n')
37-
if (lines.length >= 2 && lines[0].trim() === '---') {
38-
// Find the second --- separator
40+
// Opening fence must be first line: `---` … `---` or `--` … `--` (output FM / template FM).
41+
// Inner region must be YAML-like; otherwise the whole thing is plain content (e.g. HRs around markdown).
42+
const lines = (templateData || '').split('\n')
43+
if (lines.length < 2) return false
44+
const head = lines[0]?.trim() || ''
45+
if (head === '---') {
3946
for (let i = 1; i < lines.length; i++) {
4047
if (lines[i].trim() === '---') {
41-
// Now validate that the content between the --- markers is actually YAML-like
42-
// Extract the content between the first and second ---
43-
const frontmatterContent = lines.slice(1, i).join('\n')
44-
return isValidYamlContent(frontmatterContent)
48+
const inner = lines.slice(1, i).join('\n')
49+
return isValidYamlContent(inner)
50+
}
51+
}
52+
return false
53+
}
54+
if (head === '--') {
55+
for (let i = 1; i < lines.length; i++) {
56+
if (lines[i].trim() === '--') {
57+
const inner = lines.slice(1, i).join('\n')
58+
return isValidYamlContent(inner)
4559
}
4660
}
61+
return false
4762
}
4863
return false
4964
}

‎np.Templating/plugin.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
"noteplan.minAppVersion": "3.21",
44
"plugin.id": "np.Templating",
55
"plugin.name": "📒 Templating",
6-
"plugin.version": "2.4.1",
6+
"plugin.version": "2.4.2",
77
"plugin.releaseStatus": "beta",
8-
"plugin.lastUpdateInfo": "2.4.1: Fix TemplateRunner replaceHeading with <current> so location: replace + replaceHeading: true removes the target heading.\n2.4.0: TemplateRunner getNoteTitled calendar targets — rendered dates (YYYY-MM-DD, YYYY-MM, YYYY-Qn, YYYY) and new relative tokens; preserves YAML false/0 in frontmatter parsing.\n2.3.2: Frontmatter onePromptAtATime / batchPrompts false — disable auto Command Bar batching per template.\n2.3.1: promptForm({ ... }) — explicit single-tag Command Bar form; docs on PromptCommandBarForms / PromptCommands.\n2.3.0: Minor release — min NotePlan 3.21; Command Bar multi-field forms for batched prompts. Docs: https://noteplan.co/templates/docs\n2.2.10: templateRunner run-from-code for newNoteTitle; return null when nothing executed so Forms shows error.\n2.2.9: Add triggerTemplateRunner command to automatically run templates when notes are opened\n- New hidden command triggerTemplateRunner checks for runTemplateOnOpen frontmatter attribute\n- When a note with runTemplateOnOpen is opened, the specified template is automatically executed\n- Use triggers: onOpen => np.Templating.triggerTemplateRunner in note frontmatter to enable\n2.2.7: templateRunnerExecute now returns AI analysis errors from all code paths\n- Modified templateRunnerExecute to return Promise<string | void> instead of Promise<void>\n- Added hasAiAnalysisError helper function to detect AI analysis error marker\n- Updated all handler functions (handleCurrentNote, handleRegularNote, handleTodayNote, handleWeeklyNote) to return errors\n- Updated handleNewNoteCreation to return rendered content if it contains AI analysis error\n- All early return paths in templateRunnerExecute now check for and return AI analysis errors\n- This allows Forms plugin to capture and display template rendering errors to users\n2.2.6: (previous version)\n2.2.5: fix messaging in templateNew when templates not found\n- Support template:ignore code blocks in templates\n2.2.4: fix <default> templateLocale setting to allow for locale-specific date/time formatting\n2.2.3: add new weather formatting fields (location.*, :formatted: etc.",
8+
"plugin.lastUpdateInfo": "2.4.2: Output frontmatter only when body starts with --/--- and inner is YAML-like; --- pairs ignore embedded -- lines; getSanitizedFmParts parses leading -- fences; isValidYamlContent skips ATX headings (--- HR around markdown).\n2.4.1: Fix TemplateRunner replaceHeading with <current> so location: replace + replaceHeading: true removes the target heading; TemplateRunner debug logging for large bodies.\n2.4.0: TemplateRunner getNoteTitled calendar targets — rendered dates (YYYY-MM-DD, YYYY-MM, YYYY-Qn, YYYY) and new relative tokens; preserves YAML false/0 in frontmatter parsing.\n2.3.2: Frontmatter onePromptAtATime / batchPrompts false — disable auto Command Bar batching per template.\n2.3.1: promptForm({ ... }) — explicit single-tag Command Bar form; docs on PromptCommandBarForms / PromptCommands.\n2.3.0: Minor release — min NotePlan 3.21; Command Bar multi-field forms for batched prompts. Docs: https://noteplan.co/templates/docs\n2.2.10: templateRunner run-from-code for newNoteTitle; return null when nothing executed so Forms shows error.\n2.2.9: Add triggerTemplateRunner command to automatically run templates when notes are opened\n- New hidden command triggerTemplateRunner checks for runTemplateOnOpen frontmatter attribute\n- When a note with runTemplateOnOpen is opened, the specified template is automatically executed\n- Use triggers: onOpen => np.Templating.triggerTemplateRunner in note frontmatter to enable\n2.2.7: templateRunnerExecute now returns AI analysis errors from all code paths\n- Modified templateRunnerExecute to return Promise<string | void> instead of Promise<void>\n- Added hasAiAnalysisError helper function to detect AI analysis error marker\n- Updated all handler functions (handleCurrentNote, handleRegularNote, handleTodayNote, handleWeeklyNote) to return errors\n- Updated handleNewNoteCreation to return rendered content if it contains AI analysis error\n- All early return paths in templateRunnerExecute now check for and return AI analysis errors\n- This allows Forms plugin to capture and display template rendering errors to users\n2.2.6: (previous version)\n2.2.5: fix messaging in templateNew when templates not found\n- Support template:ignore code blocks in templates\n2.2.4: fix <default> templateLocale setting to allow for locale-specific date/time formatting\n2.2.3: add new weather formatting fields (location.*, :formatted: etc.",
99
"plugin.description": "Templating Plugin for NotePlan",
1010
"plugin.author": "Mike Erickson (@codedungeon) and David Wertheimer (@dwertheimer)",
1111
"plugin.dependencies": [],

0 commit comments

Comments
 (0)