From d4423ad0dc8cb40b2beab437fc93327efa136b97 Mon Sep 17 00:00:00 2001 From: Ricky White Date: Sun, 11 Oct 2026 08:08:56 -0400 Subject: [PATCH 1/2] feat: support Django and Jinja language identifiers --- README.md | 17 +++++++-- package.json | 24 +++++++++++++ scripts/validate-snippets.js | 68 +++++++++++++++++++++++++++++++++++- 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 37d66c8..21e96df 100644 --- a/README.md +++ b/README.md @@ -63,12 +63,25 @@ ptfor → Tab → Fill in the blanks! ## 🎯 Quick Start -1. Open any HTML file in VS Code +1. Open an HTML, Django, or Jinja template file in VS Code 2. Type `pt` to see available snippets 3. Select a snippet from the IntelliSense menu (or type the full prefix) 4. Press `Tab` to trigger the snippet 5. Fill in the placeholders and press `Tab` to jump between them +### Supported Language Modes + +Snippets are available according to the file's VS Code language identifier: + +| Language ID | General snippets | Jinja snippets | Django snippets | +|-------------|------------------|----------------|-----------------| +| `html` | Yes | Yes | Yes | +| `django-html` | Yes | No | Yes | +| `jinja` | Yes | Yes | No | +| `jinja-html` | Yes | Yes | No | + +For automatic template language recognition, you may need a language extension such as [Django](https://marketplace.visualstudio.com/items?itemName=batisteo.vscode-django), [Jinja](https://marketplace.visualstudio.com/items?itemName=wholroyd.jinja), or [Better Jinja](https://marketplace.visualstudio.com/items?itemName=samuelcolvin.jinjahtml). You can also select a supported language mode from VS Code's status bar once its language extension is installed. Filename extensions such as `.j2` depend on that extension's file associations; this snippet extension does not change them. + ### 💡 Pro Tip Enable tab completion in your VS Code settings for the best experience: ```json @@ -185,7 +198,7 @@ Found a bug? Want a new snippet? Contributions are welcome! Every pull request runs a small validation suite on Node.js 20: 1. `npm ci` -2. `npm test` (JSON + snippet metadata validation) +2. `npm test` (JSON + snippet metadata and language registration validation) 3. `npx vsce package` If you want to mirror CI locally before opening a PR, run the same commands from the repository root. diff --git a/package.json b/package.json index c5cfab5..fe2aa2e 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,30 @@ { "language": "html", "path": "./snippets/django-snippets.json" + }, + { + "language": "django-html", + "path": "./snippets/snippets.json" + }, + { + "language": "django-html", + "path": "./snippets/django-snippets.json" + }, + { + "language": "jinja", + "path": "./snippets/snippets.json" + }, + { + "language": "jinja", + "path": "./snippets/jinja-snippets.json" + }, + { + "language": "jinja-html", + "path": "./snippets/snippets.json" + }, + { + "language": "jinja-html", + "path": "./snippets/jinja-snippets.json" } ] }, diff --git a/scripts/validate-snippets.js b/scripts/validate-snippets.js index a2ae8fd..907924f 100644 --- a/scripts/validate-snippets.js +++ b/scripts/validate-snippets.js @@ -36,6 +36,72 @@ function validateRequiredString(snippet, fieldName) { return typeof snippet[fieldName] === "string" && snippet[fieldName].trim().length > 0; } +const expectedRegistrations = { + html: catalogs.map((catalog) => catalog.path), + "django-html": ["snippets/snippets.json", "snippets/django-snippets.json"], + jinja: ["snippets/snippets.json", "snippets/jinja-snippets.json"], + "jinja-html": ["snippets/snippets.json", "snippets/jinja-snippets.json"], +}; +const seenRegistrations = new Set(); + +try { + const manifest = JSON.parse(fs.readFileSync("package.json", "utf8")); + const registrations = manifest.contributes && manifest.contributes.snippets; + + if (!Array.isArray(registrations)) { + addError("package.json: contributes.snippets must be an array"); + } else { + for (const registration of registrations) { + if (!registration || typeof registration !== "object" || Array.isArray(registration)) { + addError("package.json: each snippet registration must be an object"); + continue; + } + + const { language, path: snippetPath } = registration; + if ( + !validateRequiredString(registration, "language") || + !Object.hasOwn(expectedRegistrations, language) + ) { + addError(`package.json: invalid or unsupported snippet language "${language}"`); + continue; + } + if (!validateRequiredString(registration, "path")) { + addError(`package.json: missing or invalid snippet path for "${language}"`); + continue; + } + + const normalizedPath = path.normalize(snippetPath); + const key = `${language}:${normalizedPath}`; + if (seenRegistrations.has(key)) { + addError(`package.json: duplicate snippet registration "${key}"`); + } + seenRegistrations.add(key); + + try { + if (!fs.statSync(snippetPath).isFile()) { + addError(`package.json: snippet path "${snippetPath}" must be a file`); + } + } catch (error) { + addError(`package.json: failed to read snippet file "${snippetPath}" (${error.message})`); + } + + if (!expectedRegistrations[language].includes(normalizedPath)) { + addError(`package.json: unexpected snippet file "${snippetPath}" for "${language}"`); + } + } + } +} catch (error) { + addError(`package.json: failed to read manifest (${error.message})`); +} + +for (const [language, snippetPaths] of Object.entries(expectedRegistrations)) { + for (const snippetPath of snippetPaths) { + if (!seenRegistrations.has(`${language}:${snippetPath}`)) { + addError(`package.json: missing snippet registration "${language}:${snippetPath}"`); + } + } +} + for (const catalog of catalogs) { const absolutePath = path.resolve(catalog.path); let parsedCatalog; @@ -112,4 +178,4 @@ if (errors.length > 0) { process.exit(1); } -console.log(`Snippet validation passed (${catalogs.length} catalogs, ${seenPrefixes.size} prefixes checked).`); +console.log(`Snippet validation passed (${catalogs.length} catalogs, ${seenPrefixes.size} prefixes, ${seenRegistrations.size} registrations checked).`); From 0e0427325b00dd039df261d319d037f02aac5429 Mon Sep 17 00:00:00 2001 From: Ricky White Date: Sun, 11 Oct 2026 08:26:17 -0400 Subject: [PATCH 2/2] Normalize snippet paths to use forward slashes Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/validate-snippets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/validate-snippets.js b/scripts/validate-snippets.js index 907924f..cec687b 100644 --- a/scripts/validate-snippets.js +++ b/scripts/validate-snippets.js @@ -70,7 +70,7 @@ try { continue; } - const normalizedPath = path.normalize(snippetPath); + const normalizedPath = path.normalize(snippetPath).replaceAll(path.sep, "/"); const key = `${language}:${normalizedPath}`; if (seenRegistrations.has(key)) { addError(`package.json: duplicate snippet registration "${key}"`);