From 7d2ecc53afb4c3ab23c6baded2283defd0337e9c Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Sat, 14 Mar 2026 11:07:36 +0100 Subject: [PATCH] Add front matter validation setting and update schema validation logic --- package.json | 6 +++ package.nls.json | 1 + src/commands/StatusListener.ts | 69 ++++++++++++++++++++++++++++++---- src/constants/settings.ts | 2 + 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index edee295a..94776ba8 100644 --- a/package.json +++ b/package.json @@ -2110,6 +2110,12 @@ "markdownDescription": "%setting.frontMatter.templates.prefix.markdownDescription%", "scope": "Templates" }, + "frontMatter.validation.enabled": { + "type": "boolean", + "default": true, + "markdownDescription": "%setting.frontMatter.validation.enabled.markdownDescription%", + "scope": "Validation" + }, "frontMatter.website.host": { "type": "string", "markdownDescription": "%setting.frontMatter.website.host.markdownDescription%" diff --git a/package.nls.json b/package.nls.json index 8110f097..0b07a3a6 100644 --- a/package.nls.json +++ b/package.nls.json @@ -277,6 +277,7 @@ "setting.frontMatter.taxonomy.tags.markdownDescription": "Specifies the tags which can be used in the Front Matter. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.tags) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.tags%22%5D)", "setting.frontMatter.telemetry.disable.markdownDescription": "Specify if you want to disable the telemetry. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.telemetry.disable) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.telemetry.disable%22%5D)", "setting.frontMatter.templates.enabled.markdownDescription": "Specify if you want to use templates. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.enabled) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.templates.enabled%22%5D)", + "setting.frontMatter.validation.enabled.markdownDescription": "Specify if you want to enable front matter validation. When enabled, the extension will validate your front matter against the content type schema. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.validation.enabled) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.validation.enabled%22%5D)", "setting.frontMatter.templates.folder.markdownDescription": "Specify the folder to use for your article templates. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.folder) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.templates.folder%22%5D)", "setting.frontMatter.templates.prefix.markdownDescription": "Specify the prefix you want to add for your new article filenames. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.prefix) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.templates.prefix%22%5D)", "setting.frontMatter.dashboard.mediaSnippet.deprecationMessage": "This setting is deprecated and will be removed in the next major version. Please define your media snippet in the `frontMatter.content.snippet` setting.", diff --git a/src/commands/StatusListener.ts b/src/commands/StatusListener.ts index 5aded95e..477aa904 100644 --- a/src/commands/StatusListener.ts +++ b/src/commands/StatusListener.ts @@ -4,7 +4,8 @@ import { EXTENSION_NAME, NOTIFICATION_TYPE, SETTING_SEO_DESCRIPTION_LENGTH, - SETTING_SEO_TITLE_LENGTH + SETTING_SEO_TITLE_LENGTH, + SETTING_VALIDATION_ENABLED } from './../constants'; import * as vscode from 'vscode'; import { ArticleHelper, Notifications, SeoHelper, Settings, FrontMatterValidator } from '../helpers'; @@ -20,7 +21,13 @@ import { i18n } from './i18n'; import { getDescriptionField, getTitleField } from '../utils'; export class StatusListener { - private static validator: FrontMatterValidator = new FrontMatterValidator(); + private static _validator: FrontMatterValidator | undefined; + private static get validator(): FrontMatterValidator { + if (!StatusListener._validator) { + StatusListener._validator = new FrontMatterValidator(); + } + return StatusListener._validator; + } /** * Update the text of the status bar * @@ -73,7 +80,10 @@ export class StatusListener { StatusListener.verifyRequiredFields(editor, article, collection); // Schema validation - await StatusListener.verifySchemaValidation(editor, article, collection); + const validationEnabled = Settings.get(SETTING_VALIDATION_ENABLED, true); + if (validationEnabled) { + await StatusListener.verifySchemaValidation(editor, article, collection); + } } } @@ -211,12 +221,21 @@ export class StatusListener { for (const error of errors) { // For required field errors, use the missing property name let fieldName = ''; + let arrayIndex: number | undefined; if (error.keyword === 'required' && error.params?.missingProperty) { fieldName = error.params.missingProperty; } else { // Find the field in the document const fieldPath = error.field.split('.'); - fieldName = fieldPath[fieldPath.length - 1]; + // If the last segment is a numeric index (e.g. tags.0), use the parent + // field name and track which array item to highlight + const lastSegment = fieldPath[fieldPath.length - 1]; + if (/^\d+$/.test(lastSegment)) { + arrayIndex = parseInt(lastSegment, 10); + fieldName = fieldPath[fieldPath.length - 2] || ''; + } else { + fieldName = lastSegment; + } } if (!fieldName || fieldName === 'root') { @@ -224,14 +243,48 @@ export class StatusListener { } // Try to find the field location in the front matter section only - // Note: This is a simple implementation that may match partial strings - // Future improvement: Use YAML AST parsing for exact field locations const searchText = text.substring(0, frontMatterEnd); const fieldIdx = searchText.indexOf(`${fieldName}:`); if (fieldIdx !== -1) { - const posStart = editor.document.positionAt(fieldIdx); - const posEnd = editor.document.positionAt(fieldIdx + fieldName.length); + let posStart: vscode.Position; + let posEnd: vscode.Position; + + // Default range: the field name itself + posStart = editor.document.positionAt(fieldIdx); + posEnd = editor.document.positionAt(fieldIdx + fieldName.length); + + if (arrayIndex !== undefined) { + // Walk lines after the field to find the Nth array item (lines starting with ' - ') + const afterField = text.indexOf('\n', fieldIdx) + 1; + let remaining = arrayIndex; + let searchFrom = afterField; + while (searchFrom < frontMatterEnd) { + const lineEnd = text.indexOf('\n', searchFrom); + const line = text.substring(searchFrom, lineEnd === -1 ? frontMatterEnd : lineEnd); + if (/^\s*-\s/.test(line)) { + if (remaining === 0) { + // Found the right item — highlight the value after '- ' + const valueOffset = line.indexOf('- ') + 2; + const rawItemValue = line.substring(valueOffset).trim(); + const isQuoted = + rawItemValue.length > 1 && + ((rawItemValue.startsWith('"') && rawItemValue.endsWith('"')) || + (rawItemValue.startsWith('\'') && rawItemValue.endsWith('\''))); + const itemValue = isQuoted ? rawItemValue.slice(1, -1) : rawItemValue; + const valueStartOffset = searchFrom + valueOffset + (isQuoted ? 1 : 0); + posStart = editor.document.positionAt(valueStartOffset); + posEnd = editor.document.positionAt(valueStartOffset + itemValue.length); + break; + } + remaining--; + } else if (line.trim() && !/^\s/.test(line)) { + // Hit a new top-level field — stop searching + break; + } + searchFrom = (lineEnd === -1 ? frontMatterEnd : lineEnd) + 1; + } + } const diagnostic: vscode.Diagnostic = { code: '', diff --git a/src/constants/settings.ts b/src/constants/settings.ts index 6c96af7e..be7a12f7 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -120,6 +120,8 @@ export const SETTING_COPILOT_FAMILY = 'copilot.family'; export const SETTING_LOGGING = 'logging'; +export const SETTING_VALIDATION_ENABLED = 'validation.enabled'; + /** * Sponsors only settings */