Add front matter validation setting and update schema validation logic

This commit is contained in:
Elio Struyf
2026-03-14 11:07:36 +01:00
parent 5de91cf683
commit 7d2ecc53af
4 changed files with 70 additions and 8 deletions
+6
View File
@@ -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%"
+1
View File
@@ -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.",
+61 -8
View File
@@ -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<boolean>(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: '',
+2
View File
@@ -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
*/