@grailwind/eslint-plugin 0.1.0-beta.3 → 0.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -10
- package/package.json +2 -2
- package/src/blazon.js +58 -11
- package/src/index.js +2 -0
- package/src/rules/field-in-shield.js +45 -0
- package/src/rules/modifier-needs-target.js +1 -1
- package/src/rules/rule-of-tincture.js +14 -4
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install --save-dev @grailwind/eslint-plugin@beta
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
It reads the tinctures, ordinaries and charges from your installed `grailwind`, so it needs
|
|
16
|
-
`grailwind` 0.
|
|
16
|
+
`grailwind` 0.9.x. For typos and other class errors, see
|
|
17
17
|
[Linting](https://github.com/pafirmin/grailwind#linting) in the Grailwind README.
|
|
18
18
|
|
|
19
19
|
## Setup
|
|
@@ -57,7 +57,7 @@ export default [
|
|
|
57
57
|
No metal (`or`, `argent`) on metal, and no colour on colour.
|
|
58
58
|
|
|
59
59
|
- An ordinary, chief or bordure is checked against the field on the same element:
|
|
60
|
-
`
|
|
60
|
+
`or fess-argent` is metal on metal. So are engrailed, invected, wavy, indented, embattled and voided ones
|
|
61
61
|
(`fess-engrailed-argent`, `cross-voided-argent`).
|
|
62
62
|
- A fimbriated ordinary is checked against its fimbriation, and the fimbriation against the
|
|
63
63
|
field: `or chevron-sable fimbriated-argent` is metal on metal (Argent on Or).
|
|
@@ -66,26 +66,30 @@ No metal (`or`, `argent`) on metal, and no colour on colour.
|
|
|
66
66
|
- Charges at `in-dexter-chief`, `in-chief-point` or `in-sinister-chief`, or in a
|
|
67
67
|
`charges in-chief` container, are checked against the chief, if there is one.
|
|
68
68
|
- A plain canton or inescutcheon, one with nothing on it, is checked against the field it lies
|
|
69
|
-
on. One with charges, an ordinary or a division
|
|
69
|
+
on. An inescutcheon's field is its one child. One with charges, an ordinary or a division
|
|
70
|
+
counts as arms of its own and is exempt.
|
|
70
71
|
|
|
71
72
|
A divided field (`per-pale-or-gules`) has no single tincture, so nothing is checked against it.
|
|
72
73
|
Neither are `<img>` charges, charges with no tincture, or `bordure-*` with a non-heraldic colour.
|
|
73
74
|
Counterchanged ordinaries and charges take the field's tinctures swapped, so they're skipped too.
|
|
74
75
|
|
|
75
|
-
`of-the-first` is checked as the shield's first tincture,
|
|
76
|
+
`of-the-first` is checked as the shield's first tincture (a quarter's own, but not a canton's or
|
|
77
|
+
an inescutcheon's), and reported if it can't be seen.
|
|
76
78
|
|
|
77
79
|
Furs count by their ground: `ermine` and `erminois` as metals, `ermines` and `pean` as colours.
|
|
78
|
-
`ermine fess-argent` is metal on metal.
|
|
80
|
+
`ermine fess-argent` is metal on metal. The vair family (`vair`, `potent`, `vairy-or-gules` and
|
|
81
|
+
the like) has no single ground, so it isn't checked. Authorities differ, so the `furs` option
|
|
82
|
+
changes this:
|
|
79
83
|
|
|
80
84
|
```js
|
|
81
85
|
"grailwind/rule-of-tincture": ["error", { furs: "ground" }] // the default
|
|
82
86
|
"grailwind/rule-of-tincture": ["error", { furs: "exempt" }] // furs go with anything
|
|
83
87
|
"grailwind/rule-of-tincture": ["error", {
|
|
84
|
-
furs: { ermine: "metal", erminois: "exempt", ermines: "colour", pean: "colour" },
|
|
88
|
+
furs: { ermine: "metal", erminois: "exempt", ermines: "colour", pean: "colour", vair: "metal" },
|
|
85
89
|
}]
|
|
86
90
|
```
|
|
87
91
|
|
|
88
|
-
A fur left out of the map counts by
|
|
92
|
+
A fur left out of the map counts as it does by default.
|
|
89
93
|
|
|
90
94
|
### `charges-in-container`
|
|
91
95
|
|
|
@@ -94,6 +98,17 @@ container.
|
|
|
94
98
|
|
|
95
99
|
An arrangement (`in-chief`, `in-pale`, `between` and so on) goes on the container, not a charge.
|
|
96
100
|
|
|
101
|
+
### `field-in-shield`
|
|
102
|
+
|
|
103
|
+
A `heater` or `inescutcheon` is a frame round one child, the field, which carries the blazon.
|
|
104
|
+
This flags a tincture, division, ordinary, chief, bordure or `fimbriated-*` on the frame, as in
|
|
105
|
+
`<div class="heater azure">`, and a `charges` container directly in it, which would be taken for
|
|
106
|
+
the field. It also flags `escutcheon`, which is now `heater`:
|
|
107
|
+
|
|
108
|
+
```html
|
|
109
|
+
<div class="heater"><div class="azure fess-or"></div></div>
|
|
110
|
+
```
|
|
111
|
+
|
|
97
112
|
### `arrangement-needs-ordinary`
|
|
98
113
|
|
|
99
114
|
`charges between` and `charges on` take their positions from the ordinary on the shield. Without
|
|
@@ -104,7 +119,8 @@ one, other than a chief, they have nowhere to go.
|
|
|
104
119
|
An element draws one ordinary plus, optionally, a chief. With two, such as
|
|
105
120
|
`fess-azure chevron-gules`, only one shows. Two chiefs are flagged too. A `charges` container
|
|
106
121
|
takes one arrangement: `between`, `on`, `in-pale`, `in-fess`, `in-bend` or `in-bend-sinister`.
|
|
107
|
-
Counterchanged, engrailed, invected, voided and fur ordinaries count as
|
|
122
|
+
Counterchanged, engrailed, invected, wavy, indented, embattled, voided and fur ordinaries count as
|
|
123
|
+
ordinaries.
|
|
108
124
|
|
|
109
125
|
A charge at a named point (`in-dexter-chief` and so on) is a separate group in the blazon, so it
|
|
110
126
|
goes in its own `charges` container. Among other charges, it still counts towards their number
|
|
@@ -119,8 +135,8 @@ Modifiers that would otherwise do nothing:
|
|
|
119
135
|
element, e.g. `per-pale-argent-gules`.
|
|
120
136
|
- `counterchanged` on `charges`, or on a charge, needs a divided field on the shield that holds
|
|
121
137
|
the container.
|
|
122
|
-
- `fimbriated-*` needs a plain tinctured ordinary. A chief, and an engrailed, invected,
|
|
123
|
-
fur or counterchanged ordinary, aren't fimbriated.
|
|
138
|
+
- `fimbriated-*` needs a plain tinctured ordinary. A chief, and an engrailed, invected, wavy,
|
|
139
|
+
indented, embattled, voided, fur or counterchanged ordinary, aren't fimbriated.
|
|
124
140
|
- `counterchanged` doesn't work on a charge with a transform (`rotate-*`, `scale-*`, `skew-*`,
|
|
125
141
|
`translate-*`): the charge shows its own small copy of the field instead.
|
|
126
142
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grailwind/eslint-plugin",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.5",
|
|
4
4
|
"description": "ESLint rules for Grailwind heraldry markup: the rule of tincture and charge structure",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"@html-eslint/parser": "*",
|
|
28
28
|
"eslint": "^9.0.0 || ^10.0.0",
|
|
29
|
-
"grailwind": "^0.
|
|
29
|
+
"grailwind": "^0.9.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependenciesMeta": {
|
|
32
32
|
"@html-eslint/parser": {
|
package/src/blazon.js
CHANGED
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
import { UNKNOWN } from "./tree.js";
|
|
5
5
|
import {
|
|
6
6
|
ARRANGEMENTS,
|
|
7
|
+
BORDURE_LINES,
|
|
7
8
|
CHARGES,
|
|
8
9
|
COLOURS,
|
|
9
10
|
DIVISIONS,
|
|
11
|
+
FRAMES,
|
|
12
|
+
FUR_PATTERNS,
|
|
10
13
|
FURS,
|
|
11
14
|
LINES,
|
|
15
|
+
lineOrdinaries,
|
|
12
16
|
METALS,
|
|
13
17
|
OF_THE_FIRST,
|
|
14
18
|
ORDINARIES,
|
|
@@ -37,8 +41,11 @@ const ORDINARY_CLASSES = new Map();
|
|
|
37
41
|
for (const ordinary of ORDINARIES) {
|
|
38
42
|
for (const tincture of PAINT) {
|
|
39
43
|
const fur = FUR.has(tincture);
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
// `cross-potent` is the charge, not a cross of the fur potent.
|
|
45
|
+
if (!CHARGE.has(`${ordinary}-${tincture}`)) {
|
|
46
|
+
ORDINARY_CLASSES.set(`${ordinary}-${tincture}`, { ordinary, tincture, fur });
|
|
47
|
+
}
|
|
48
|
+
for (const line of LINES.filter((l) => lineOrdinaries(l).includes(ordinary))) {
|
|
42
49
|
ORDINARY_CLASSES.set(`${ordinary}-${line}-${tincture}`, { ordinary, tincture, fur, line });
|
|
43
50
|
}
|
|
44
51
|
}
|
|
@@ -65,8 +72,17 @@ for (const ordinary of VOIDABLE) {
|
|
|
65
72
|
voided: true,
|
|
66
73
|
});
|
|
67
74
|
}
|
|
68
|
-
for (const tincture of
|
|
69
|
-
|
|
75
|
+
for (const tincture of PAINT) {
|
|
76
|
+
const fur = FUR.has(tincture);
|
|
77
|
+
ORDINARY_CLASSES.set(`bordure-${tincture}`, { ordinary: "bordure", tincture, fur });
|
|
78
|
+
for (const line of BORDURE_LINES) {
|
|
79
|
+
ORDINARY_CLASSES.set(`bordure-${line}-${tincture}`, {
|
|
80
|
+
ordinary: "bordure",
|
|
81
|
+
tincture,
|
|
82
|
+
fur,
|
|
83
|
+
line,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
// Each division class, mapped to its first tincture. Two furs in one division aren't generated.
|
|
@@ -86,19 +102,19 @@ export const isFimbriable = (o) => !o.counterchanged && !o.line && !o.fur && !o.
|
|
|
86
102
|
|
|
87
103
|
export const isFur = (tincture) => FUR.has(tincture);
|
|
88
104
|
|
|
89
|
-
|
|
105
|
+
// The ermines count by their ground. The vair family has no single ground, so it's exempt.
|
|
106
|
+
const byDefault = (fur) => (FUR_PATTERNS[fur] === "ermine" ? kind(FURS[fur][0]) : "exempt");
|
|
90
107
|
|
|
91
108
|
// What the rule's `furs` option makes of one fur: "metal", "colour" or "exempt". The rule's
|
|
92
109
|
// schema has already checked the option.
|
|
93
110
|
const furChoice = (fur, option) => {
|
|
94
111
|
if (option === "ground") {
|
|
95
|
-
return
|
|
112
|
+
return byDefault(fur);
|
|
96
113
|
}
|
|
97
114
|
if (option === "exempt") {
|
|
98
115
|
return "exempt";
|
|
99
116
|
}
|
|
100
|
-
|
|
101
|
-
return option[fur] ?? groundKind(fur);
|
|
117
|
+
return option[fur] ?? byDefault(fur);
|
|
102
118
|
};
|
|
103
119
|
|
|
104
120
|
// Each fur's kind for kind() and sameKind(). Exempt is null, which they treat as "no check".
|
|
@@ -145,6 +161,36 @@ export function ordinariesOf(classes) {
|
|
|
145
161
|
|
|
146
162
|
export const isDivided = (classes) => [...classes].some((cls) => DIVISION_CLASSES.has(cls));
|
|
147
163
|
|
|
164
|
+
/**
|
|
165
|
+
* The frame an element is, e.g. `heater` or `inescutcheon`, or undefined.
|
|
166
|
+
* @param {Set<string>} classes The element's classes.
|
|
167
|
+
* @returns {string|undefined} The frame's class.
|
|
168
|
+
*/
|
|
169
|
+
export const frameOf = (classes) => FRAMES.find((frame) => classes.has(frame));
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The classes that belong on a field: its tincture or division, its ordinaries, chief and
|
|
173
|
+
* bordure, and their fimbriation.
|
|
174
|
+
* @param {Set<string>} classes The element's classes.
|
|
175
|
+
* @returns {string[]} Those of `classes` that do.
|
|
176
|
+
*/
|
|
177
|
+
export const fieldClassesOf = (classes) =>
|
|
178
|
+
[...classes].filter(
|
|
179
|
+
(cls) =>
|
|
180
|
+
PAINT.has(cls) ||
|
|
181
|
+
DIVISIONS.includes(cls) ||
|
|
182
|
+
DIVISION_CLASSES.has(cls) ||
|
|
183
|
+
ORDINARY_CLASSES.has(cls) ||
|
|
184
|
+
FIMBRIATIONS.has(cls),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A frame's field: its one child element.
|
|
189
|
+
* @param {object} el A frame, from getElements().
|
|
190
|
+
* @returns {object|null} The field, or null if the frame hasn't exactly one child element.
|
|
191
|
+
*/
|
|
192
|
+
export const fieldOf = (el) => (el.children.length === 1 ? el.children[0] : null);
|
|
193
|
+
|
|
148
194
|
// The single bare tincture of a plain field. A divided field, or none, has no tincture.
|
|
149
195
|
export function fieldTincture(classes) {
|
|
150
196
|
if (isDivided(classes)) {
|
|
@@ -191,11 +237,12 @@ const fieldFirst = (classes) => {
|
|
|
191
237
|
return division ? DIVISION_CLASSES.get(division) : fieldTincture(classes);
|
|
192
238
|
};
|
|
193
239
|
|
|
194
|
-
// `of-the-first` on an element: the nearest
|
|
195
|
-
// Null if the shield can't be seen.
|
|
240
|
+
// `of-the-first` on an element: the nearest shield's or quarter's first (not a canton's, nor an
|
|
241
|
+
// inescutcheon's). A shield's field is the child of its frame. Null if the shield can't be seen.
|
|
196
242
|
export function firstOf(el) {
|
|
197
243
|
for (let node = el; node && node !== UNKNOWN; node = node.parent) {
|
|
198
|
-
|
|
244
|
+
const frame = node.parent?.classes && frameOf(node.parent.classes);
|
|
245
|
+
if ((frame && frame !== "inescutcheon") || node.parent?.classes?.has("quarterly")) {
|
|
199
246
|
const first = fieldFirst(node.classes);
|
|
200
247
|
return first === OF_THE_FIRST ? null : first;
|
|
201
248
|
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import arrangementNeedsOrdinary from "./rules/arrangement-needs-ordinary.js";
|
|
3
3
|
import chargesInContainer from "./rules/charges-in-container.js";
|
|
4
|
+
import fieldInShield from "./rules/field-in-shield.js";
|
|
4
5
|
import modifierNeedsTarget from "./rules/modifier-needs-target.js";
|
|
5
6
|
import oneOrdinary from "./rules/one-ordinary.js";
|
|
6
7
|
import ruleOfTincture from "./rules/rule-of-tincture.js";
|
|
@@ -17,6 +18,7 @@ const plugin = {
|
|
|
17
18
|
"arrangement-needs-ordinary": arrangementNeedsOrdinary,
|
|
18
19
|
"one-ordinary": oneOrdinary,
|
|
19
20
|
"modifier-needs-target": modifierNeedsTarget,
|
|
21
|
+
"field-in-shield": fieldInShield,
|
|
20
22
|
},
|
|
21
23
|
configs: {},
|
|
22
24
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getElements } from "../tree.js";
|
|
2
|
+
import { fieldClassesOf, frameOf } from "../blazon.js";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description: "Require the blazon on a frame's one child, the field, not on the frame",
|
|
9
|
+
url: "https://github.com/pafirmin/grailwind/tree/main/packages/eslint-plugin#field-in-shield",
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
onFrame: "`{{cls}}` goes on the {{frame}}'s one child, the field",
|
|
14
|
+
charges: "`charges` goes in the {{frame}}'s one child, the field, not on the {{frame}}",
|
|
15
|
+
escutcheon: "`escutcheon` is now `heater`, with the blazon on its one child",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
Program() {
|
|
22
|
+
for (const el of getElements(context.sourceCode)) {
|
|
23
|
+
if (!el.classes) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (el.classes.has("escutcheon")) {
|
|
27
|
+
context.report({ node: el.reportNode, messageId: "escutcheon" });
|
|
28
|
+
}
|
|
29
|
+
const frame = frameOf(el.classes);
|
|
30
|
+
if (!frame) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
for (const cls of fieldClassesOf(el.classes)) {
|
|
34
|
+
context.report({ node: el.reportNode, messageId: "onFrame", data: { cls, frame } });
|
|
35
|
+
}
|
|
36
|
+
for (const child of el.children) {
|
|
37
|
+
if (child.classes?.has("charges")) {
|
|
38
|
+
context.report({ node: child.reportNode, messageId: "charges", data: { frame } });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -15,7 +15,7 @@ export default {
|
|
|
15
15
|
messages: {
|
|
16
16
|
noDivision: "`{{cls}}` needs a divided field to swap, e.g. `per-pale-argent-gules`",
|
|
17
17
|
noOrdinary:
|
|
18
|
-
"`{{cls}}` needs a plain tinctured ordinary to edge: a chief, and an engrailed, invected, fur or counterchanged ordinary, aren't fimbriated",
|
|
18
|
+
"`{{cls}}` needs a plain tinctured ordinary to edge: a chief, and an engrailed, invected, wavy, indented, embattled, fur or counterchanged ordinary, aren't fimbriated",
|
|
19
19
|
transformed:
|
|
20
20
|
"`{{cls}}` breaks `counterchanged`: a transformed charge shows its own small copy of the field",
|
|
21
21
|
},
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
chargeName,
|
|
5
5
|
chargeTincture,
|
|
6
6
|
describe,
|
|
7
|
+
fieldOf,
|
|
7
8
|
fieldTincture,
|
|
8
9
|
fimbriationOf,
|
|
9
10
|
firstOf,
|
|
@@ -118,11 +119,19 @@ export default {
|
|
|
118
119
|
const { ordinaries, chiefs, bordures } = ordinariesOf(el.classes);
|
|
119
120
|
const fimbriation = fimbriationOf(el.classes);
|
|
120
121
|
|
|
121
|
-
// A plain sub-shield lies on its parent's field. A charged or divided one is arms.
|
|
122
|
+
// A plain sub-shield lies on its parent's field. A charged or divided one is arms. An
|
|
123
|
+
// inescutcheon is a frame, so its field is its one child.
|
|
122
124
|
const sub = ["canton", "inescutcheon"].find((c) => el.classes.has(c));
|
|
123
|
-
|
|
125
|
+
const subField = sub && (sub === "inescutcheon" ? fieldOf(el) : el);
|
|
126
|
+
if (
|
|
127
|
+
subField?.classes &&
|
|
128
|
+
isPlain(subField) &&
|
|
129
|
+
el.parent !== UNKNOWN &&
|
|
130
|
+
el.parent.classes
|
|
131
|
+
) {
|
|
124
132
|
const ground = fieldTincture(el.parent.classes);
|
|
125
|
-
|
|
133
|
+
const tincture = fieldTincture(subField.classes);
|
|
134
|
+
check(el, subField.reportNode, tincture, `plain ${sub}`, tincture, ground, "plain");
|
|
126
135
|
}
|
|
127
136
|
const isFimbriated = fimbriation && ordinaries.some(isFimbriable);
|
|
128
137
|
|
|
@@ -154,7 +163,8 @@ export default {
|
|
|
154
163
|
if (!charge.classes || charge.tag === "img") {
|
|
155
164
|
continue;
|
|
156
165
|
}
|
|
157
|
-
// Counterchanged charges take the swapped
|
|
166
|
+
// Counterchanged charges take the field's tinctures (swapped, or its own on a counterchanged
|
|
167
|
+
// ordinary), not one of their own.
|
|
158
168
|
if (charge.classes.has("counterchanged") || container.classes.has("counterchanged")) {
|
|
159
169
|
continue;
|
|
160
170
|
}
|