Now formatted correctly with eslint
This commit is contained in:
24
tools/eslint-rules.js
Normal file
24
tools/eslint-rules.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Custom ESLint rules for Angular template formatting
|
||||
// This enforces the specific formatting style for Angular templates
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
'angular-template-spacing': {
|
||||
meta: {
|
||||
type: 'layout',
|
||||
docs: {
|
||||
description: 'Enforce spacing between elements and property grouping in Angular templates',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'whitespace',
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
// This is a placeholder for custom rule implementation
|
||||
// ESLint's template rules are limited, so manual formatting is recommended
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
138
tools/format-templates.js
Normal file
138
tools/format-templates.js
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Custom Angular Template Formatter
|
||||
* Enforces specific formatting rules for Angular HTML templates:
|
||||
* 1. Blank lines between sibling elements (except first)
|
||||
* 2. Property grouping: outputs → two-way → inputs → attributes
|
||||
* 3. Multi-line properties when 3+
|
||||
* 4. Proper indentation in @if/@for/@switch blocks
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
|
||||
const PROPERTY_ORDER = {
|
||||
'STRUCTURAL_DIRECTIVE': 0,
|
||||
'TEMPLATE_REFERENCE': 1,
|
||||
'OUTPUT_BINDING': 2,
|
||||
'TWO_WAY_BINDING': 3,
|
||||
'INPUT_BINDING': 4,
|
||||
'ATTRIBUTE_BINDING': 5,
|
||||
};
|
||||
|
||||
// Detect property type
|
||||
function getPropertyType(attr) {
|
||||
if (attr.match(/^\(\w+\)/)) return 'OUTPUT_BINDING'; // (click)
|
||||
if (attr.match(/^\[\(\w+\)\]/)) return 'TWO_WAY_BINDING'; // [(ngModel)]
|
||||
if (attr.match(/^\[[\w\-.]+\]/)) return 'INPUT_BINDING'; // [property]
|
||||
if (attr.match(/^#\w+/)) return 'TEMPLATE_REFERENCE'; // #ref
|
||||
if (attr.match(/^@/)) return 'STRUCTURAL_DIRECTIVE'; // @if
|
||||
return 'ATTRIBUTE_BINDING'; // class, id, etc.
|
||||
}
|
||||
|
||||
// Sort attributes by property type
|
||||
function sortAttributes(attrs) {
|
||||
return attrs.sort((a, b) => {
|
||||
const typeA = getPropertyType(a);
|
||||
const typeB = getPropertyType(b);
|
||||
return (PROPERTY_ORDER[typeA] || 99) - (PROPERTY_ORDER[typeB] || 99);
|
||||
});
|
||||
}
|
||||
|
||||
// Format an element's attributes
|
||||
function formatAttributes(element) {
|
||||
const attrRegex = /(\s+)([\w\-\.\:\@\[\(\#\*]+(?:[\w\-\.\:\@\[\(\#\*\)"'=\s\[\]]*)?)/g;
|
||||
|
||||
// Extract all attributes
|
||||
const matches = element.match(/\s+[^\s>]+(?:="[^"]*")?/g) || [];
|
||||
if (!matches.length) return element;
|
||||
|
||||
const attrs = matches
|
||||
.map(m => m.trim())
|
||||
.filter(m => m.length > 0);
|
||||
|
||||
// Sort attributes
|
||||
const sorted = sortAttributes(attrs);
|
||||
|
||||
// Format based on count
|
||||
if (sorted.length <= 2) {
|
||||
// Keep on same line if 2 or fewer
|
||||
return element.replace(/\s+[\w\-\.\:\@\[\(\#\*][^\s>]*(?:="[^"]*")?/g, '').replace(/>/, ' ' + sorted.join(' ') + '>');
|
||||
} else {
|
||||
// Put each on new line if 3+
|
||||
const indent = ' '; // Assuming 4-space indent
|
||||
const tag = element.match(/^<\w+/)[0];
|
||||
return tag + '\n' + sorted.map(a => indent + a).join('\n') + '\n>';
|
||||
}
|
||||
}
|
||||
|
||||
// Add blank lines between siblings
|
||||
function addBlankLinesBetweenSiblings(content) {
|
||||
// Match closing and opening tags on consecutive lines
|
||||
const pattern = /(<\/\w+>)\n(?!$)(<\w+|\s*<\w+|\s*@)/gm;
|
||||
return content.replace(pattern, '$1\n\n$2');
|
||||
}
|
||||
|
||||
// Ensure @if/@for/@switch blocks are properly indented
|
||||
function formatControlFlowBlocks(content) {
|
||||
const controlFlowRegex = /@(if|for|switch|case|default)\s*\([^)]*\)\s*\{/g;
|
||||
let result = content;
|
||||
|
||||
result = result.replace(controlFlowRegex, (match) => {
|
||||
// Ensure content after { is on new line and indented
|
||||
return match.replace(/\}\s*$/m, '}\n');
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Main formatter function
|
||||
function formatAngularTemplate(content) {
|
||||
// First, add blank lines between siblings
|
||||
content = addBlankLinesBetweenSiblings(content);
|
||||
|
||||
// Ensure control flow blocks are properly formatted
|
||||
content = formatControlFlowBlocks(content);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Process a file
|
||||
function processFile(filePath) {
|
||||
try {
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
const original = content;
|
||||
|
||||
content = formatAngularTemplate(content);
|
||||
|
||||
if (content !== original) {
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
console.log(`✓ Formatted: ${filePath}`);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`✗ Error processing ${filePath}:`, error.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Main
|
||||
const args = process.argv.slice(2);
|
||||
const files = args.length > 0
|
||||
? args
|
||||
: glob.sync('src/app/**/*.html', { cwd: process.cwd() });
|
||||
|
||||
let formatted = 0;
|
||||
files.forEach(file => {
|
||||
const fullPath = path.join(process.cwd(), file);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
if (processFile(fullPath)) {
|
||||
formatted++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\nTotal files formatted: ${formatted}`);
|
||||
process.exit(formatted > 0 ? 0 : 0);
|
||||
88
tools/prettier-post-processor.js
Normal file
88
tools/prettier-post-processor.js
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Prettier Plugin Wrapper for Property Sorting
|
||||
* This file hooks into the formatting pipeline to ensure property sorting
|
||||
* happens automatically whenever Prettier runs.
|
||||
*
|
||||
* Usage: Configure in .prettierrc.json to use this as a post-processor
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Property type detection
|
||||
function getPropertyType(attrName) {
|
||||
if (attrName.match(/^\(/)) return 0; // (output)="..."
|
||||
if (attrName.match(/^\[\(/)) return 1; // [(twoWay)]="..."
|
||||
if (attrName.match(/^\[class\./)) return 2; // [class.x]="..."
|
||||
if (attrName.match(/^\[attr\./)) return 2; // [attr.x]="..."
|
||||
if (attrName.match(/^\[[\w\-]+\]/)) return 2; // [input]="..."
|
||||
if (attrName.match(/^#/)) return 1.5; // #ref
|
||||
return 3; // attributes
|
||||
}
|
||||
|
||||
// Extract attribute name (before =)
|
||||
function getAttributeName(attrString) {
|
||||
const match = attrString.match(/^([^\s=]+)/);
|
||||
return match ? match[1] : attrString;
|
||||
}
|
||||
|
||||
// Sort attributes by type
|
||||
function sortAttributes(attributes) {
|
||||
return attributes.sort((a, b) => {
|
||||
const nameA = getAttributeName(a);
|
||||
const nameB = getAttributeName(b);
|
||||
return getPropertyType(nameA) - getPropertyType(nameB);
|
||||
});
|
||||
}
|
||||
|
||||
// Format file
|
||||
function formatFile(content) {
|
||||
// Pattern: match multi-line elements with attributes
|
||||
const multiLineAttrRegex = /(<\w+[\w:\-]*)\n((?:\s+[^\s>]+(?:="[^"]*")?\n)*\s+[^\s>]+(?:="[^"]*")?)\n(\s*>)/g;
|
||||
|
||||
return content.replace(multiLineAttrRegex, (match, openTag, attrs, closeTag) => {
|
||||
// Get indentation
|
||||
const lines = match.split('\n');
|
||||
const firstLineIndent = lines[0].match(/^(\s*)</)[1];
|
||||
const attrIndent = lines[1].match(/^(\s+)/)[1];
|
||||
|
||||
// Parse attributes
|
||||
const attrLines = attrs.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
|
||||
// Sort attributes
|
||||
const sorted = sortAttributes(attrLines);
|
||||
|
||||
// Rebuild
|
||||
return openTag + '\n' + sorted.map(attr => attrIndent + attr).join('\n') + '\n' + closeTag;
|
||||
});
|
||||
}
|
||||
|
||||
// Main
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: node prettier-plugin-wrapper.js <file>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const filePath = args[0];
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`File not found: ${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
const formatted = formatFile(content);
|
||||
|
||||
if (formatted !== content) {
|
||||
fs.writeFileSync(filePath, formatted, 'utf-8');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = { formatFile };
|
||||
121
tools/sort-template-properties.js
Normal file
121
tools/sort-template-properties.js
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Angular Template Property Sorter
|
||||
* Reorders element properties after Prettier formatting
|
||||
* Order: outputs → two-way → inputs → attributes
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Property type detection - lower number = higher priority (comes first)
|
||||
function getPropertyType(attrName) {
|
||||
if (attrName.match(/^\(/)) return 0; // (output)="..." → outputs first
|
||||
if (attrName.match(/^\[\(/)) return 1; // [(twoWay)]="..." → two-way second
|
||||
if (attrName.match(/^\[class\./)) return 2; // [class.x]="..." → inputs third
|
||||
if (attrName.match(/^\[attr\./)) return 2; // [attr.x]="..." → inputs third
|
||||
if (attrName.match(/^\[[\w\-]+\]/)) return 2; // [input]="..." → inputs third
|
||||
if (attrName.match(/^#/)) return 1.5; // #ref → template reference
|
||||
if (attrName.match(/^@/)) return -1; // @structural → should not appear here
|
||||
return 3; // everything else → attributes last
|
||||
}
|
||||
|
||||
// Extract attribute name (before =)
|
||||
function getAttributeName(attrString) {
|
||||
const match = attrString.match(/^([^\s=]+)/);
|
||||
return match ? match[1] : attrString;
|
||||
}
|
||||
|
||||
// Sort attributes by type
|
||||
function sortAttributes(attributes) {
|
||||
return attributes.sort((a, b) => {
|
||||
const nameA = getAttributeName(a);
|
||||
const nameB = getAttributeName(b);
|
||||
return getPropertyType(nameA) - getPropertyType(nameB);
|
||||
});
|
||||
}
|
||||
|
||||
// Format file - simple approach: parse and rebuild with sorted attrs
|
||||
function formatFile(filePath) {
|
||||
try {
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Pattern: match multi-line elements with attributes
|
||||
// This is a conservative approach - only reorders complete multi-line attribute blocks
|
||||
const multiLineAttrRegex = /(<\w+[\w:\-]*)\n((?:\s+[^\s>]+(?:="[^"]*")?\n)*\s+[^\s>]+(?:="[^"]*")?)\n(\s*>)/g;
|
||||
|
||||
let modified = false;
|
||||
const result = content.replace(multiLineAttrRegex, (match, openTag, attrs, closeTag) => {
|
||||
// Get the indentation from the first line
|
||||
const lines = match.split('\n');
|
||||
const firstLineIndent = lines[0].match(/^(\s*)</)[1];
|
||||
const attrIndent = lines[1].match(/^(\s+)/)[1];
|
||||
|
||||
// Parse attributes
|
||||
const attrLines = attrs.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
|
||||
// Sort attributes
|
||||
const sorted = sortAttributes(attrLines);
|
||||
|
||||
// Check if we changed the order
|
||||
if (sorted.join('\n') !== attrLines.join('\n')) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Rebuild
|
||||
return openTag + '\n' + sorted.map(attr => attrIndent + attr).join('\n') + '\n' + closeTag;
|
||||
});
|
||||
|
||||
if (modified) {
|
||||
fs.writeFileSync(filePath, result, 'utf-8');
|
||||
console.log(`✓ Sorted properties: ${filePath}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(`✗ Error processing ${filePath}:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Main
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function walkDir(dir, callback) {
|
||||
const files = fs.readdirSync(dir);
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.isDirectory() && !filePath.includes('node_modules')) {
|
||||
walkDir(filePath, callback);
|
||||
} else if (file.endsWith('.html')) {
|
||||
callback(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let processed = 0;
|
||||
if (args.length > 0) {
|
||||
args.forEach(file => {
|
||||
const fullPath = path.resolve(process.cwd(), file);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
if (formatFile(fullPath)) {
|
||||
processed++;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
walkDir(path.join(process.cwd(), 'src/app'), (filePath) => {
|
||||
if (formatFile(filePath)) {
|
||||
processed++;
|
||||
}
|
||||
});
|
||||
if (processed > 0) {
|
||||
console.log(`\nTotal files with properties sorted: ${processed}`);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user