Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
},
Expand Down
68 changes: 67 additions & 1 deletion scripts/validate-snippets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).replaceAll(path.sep, "/");
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;
Expand Down Expand Up @@ -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).`);
Loading