@grailwind/eslint-plugin 0.1.0-beta.0 → 0.1.0-beta.1
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 +14 -4
- package/package.json +3 -3
- package/src/blazon.js +67 -10
- package/src/rules/charges-in-container.js +24 -3
- package/src/rules/rule-of-tincture.js +38 -10
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @grailwind/eslint-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/pafirmin/grailwind/actions/workflows/test.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@grailwind/eslint-plugin)
|
|
5
|
+
[](https://github.com/pafirmin/grailwind/blob/main/packages/eslint-plugin/LICENSE)
|
|
6
|
+
|
|
7
|
+
ESLint rules for [Grailwind](https://grailwind.dev/) markup in JSX and HTML.
|
|
4
8
|
|
|
5
9
|
> **Beta.** Rules, their options and the `recommended` config may change between releases.
|
|
6
10
|
|
|
@@ -9,7 +13,7 @@ npm install --save-dev @grailwind/eslint-plugin@beta
|
|
|
9
13
|
```
|
|
10
14
|
|
|
11
15
|
It reads the tinctures, ordinaries and charges from your installed `grailwind`, so it needs
|
|
12
|
-
`grailwind` 0.
|
|
16
|
+
`grailwind` 0.5.x. For typos and other class errors, see
|
|
13
17
|
[Linting](https://github.com/pafirmin/grailwind#linting) in the Grailwind README.
|
|
14
18
|
|
|
15
19
|
## Setup
|
|
@@ -59,13 +63,17 @@ No metal (`or`, `argent`) on metal, and no colour on colour.
|
|
|
59
63
|
field: `or chevron-sable fimbriated-argent` is metal on metal (Argent on Or).
|
|
60
64
|
- Charges in a `charges` container are checked against the field.
|
|
61
65
|
- Charges in a `charges on` container are checked against the ordinary they lie on.
|
|
62
|
-
- Charges at `in-dexter-chief`, `in-chief` or `in-sinister-chief
|
|
63
|
-
if there is one.
|
|
66
|
+
- Charges at `in-dexter-chief`, `in-chief-point` or `in-sinister-chief`, or in a
|
|
67
|
+
`charges in-chief` container, are checked against the chief, if there is one.
|
|
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 counts as arms of its own and is exempt.
|
|
64
70
|
|
|
65
71
|
A divided field (`per-pale-or-gules`) has no single tincture, so nothing is checked against it.
|
|
66
72
|
Neither are `<img>` charges, charges with no tincture, or `bordure-*` with a non-heraldic colour.
|
|
67
73
|
Counterchanged ordinaries and charges take the field's tinctures swapped, so they're skipped too.
|
|
68
74
|
|
|
75
|
+
`of-the-first` is checked as the shield's first tincture, and reported if it can't be seen.
|
|
76
|
+
|
|
69
77
|
Furs count by their ground: `ermine` and `erminois` as metals, `ermines` and `pean` as colours.
|
|
70
78
|
`ermine fess-argent` is metal on metal. Authorities differ, so the `furs` option changes this:
|
|
71
79
|
|
|
@@ -84,6 +92,8 @@ A fur left out of the map counts by its ground.
|
|
|
84
92
|
Charges (`mullet`, `bezant`, `charge-[…]` and so on) must be direct children of a `charges`
|
|
85
93
|
container.
|
|
86
94
|
|
|
95
|
+
An arrangement (`in-chief`, `in-pale`, `between` and so on) goes on the container, not a charge.
|
|
96
|
+
|
|
87
97
|
### `arrangement-needs-ordinary`
|
|
88
98
|
|
|
89
99
|
`charges between` and `charges on` take their positions from the ordinary on the shield. Without
|
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.1",
|
|
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.5.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependenciesMeta": {
|
|
32
32
|
"@html-eslint/parser": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
|
-
"homepage": "https://
|
|
44
|
+
"homepage": "https://grailwind.dev/",
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
|
47
47
|
"url": "git+https://github.com/pafirmin/grailwind.git",
|
package/src/blazon.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Reading heraldry from a set of class names.
|
|
3
3
|
*/
|
|
4
|
+
import { UNKNOWN } from "./tree.js";
|
|
4
5
|
import {
|
|
5
6
|
ARRANGEMENTS,
|
|
6
7
|
CHARGES,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
FURS,
|
|
10
11
|
LINES,
|
|
11
12
|
METALS,
|
|
13
|
+
OF_THE_FIRST,
|
|
12
14
|
ORDINARIES,
|
|
13
15
|
POINTS,
|
|
14
16
|
ROUNDELS,
|
|
@@ -20,9 +22,11 @@ const COLOUR = new Set(COLOURS);
|
|
|
20
22
|
const FUR = new Set(Object.keys(FURS));
|
|
21
23
|
// Anything a field, ordinary or charge can be painted in.
|
|
22
24
|
const TINCTURE = new Set([...TINCTURES, ...FUR]);
|
|
25
|
+
// What a class can paint in: a tincture, or the field's first.
|
|
26
|
+
const PAINT = new Set([...TINCTURE, OF_THE_FIRST]);
|
|
23
27
|
const CHARGE = new Set([...CHARGES, ...Object.keys(ROUNDELS)]);
|
|
24
|
-
const FIMBRIATIONS = new Set(TINCTURES.map((t) => `fimbriated-${t}`));
|
|
25
|
-
const CHIEF_POINTS = new Set(["in-dexter-chief", "in-chief", "in-sinister-chief"]);
|
|
28
|
+
const FIMBRIATIONS = new Set([...TINCTURES, OF_THE_FIRST].map((t) => `fimbriated-${t}`));
|
|
29
|
+
const CHIEF_POINTS = new Set(["in-dexter-chief", "in-chief-point", "in-sinister-chief"]);
|
|
26
30
|
const POINT_CLASSES = new Set(Object.keys(POINTS).map((point) => `in-${point}`));
|
|
27
31
|
export const ALL_ARRANGEMENTS = ["between", "on", ...ARRANGEMENTS];
|
|
28
32
|
|
|
@@ -30,7 +34,7 @@ export const ALL_ARRANGEMENTS = ["between", "on", ...ARRANGEMENTS];
|
|
|
30
34
|
// `bordure-or`, mapped to its parts. A counterchanged ordinary has no tincture of its own.
|
|
31
35
|
const ORDINARY_CLASSES = new Map();
|
|
32
36
|
for (const ordinary of ORDINARIES) {
|
|
33
|
-
for (const tincture of
|
|
37
|
+
for (const tincture of PAINT) {
|
|
34
38
|
const fur = FUR.has(tincture);
|
|
35
39
|
ORDINARY_CLASSES.set(`${ordinary}-${tincture}`, { ordinary, tincture, fur });
|
|
36
40
|
for (const line of LINES) {
|
|
@@ -43,17 +47,17 @@ for (const ordinary of ORDINARIES) {
|
|
|
43
47
|
counterchanged: true,
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
|
-
for (const tincture of TINCTURES) {
|
|
50
|
+
for (const tincture of [...TINCTURES, OF_THE_FIRST]) {
|
|
47
51
|
ORDINARY_CLASSES.set(`bordure-${tincture}`, { ordinary: "bordure", tincture });
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
// Two furs in one division aren't generated.
|
|
51
|
-
const DIVISION_CLASSES = new
|
|
54
|
+
// Each division class, mapped to its first tincture. Two furs in one division aren't generated.
|
|
55
|
+
const DIVISION_CLASSES = new Map();
|
|
52
56
|
for (const division of DIVISIONS) {
|
|
53
57
|
for (const a of TINCTURE) {
|
|
54
58
|
for (const b of TINCTURE) {
|
|
55
59
|
if (a !== b && !(FUR.has(a) && FUR.has(b))) {
|
|
56
|
-
DIVISION_CLASSES.
|
|
60
|
+
DIVISION_CLASSES.set(`${division}-${a}-${b}`, a);
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
}
|
|
@@ -128,17 +132,66 @@ export function fieldTincture(classes) {
|
|
|
128
132
|
if (isDivided(classes)) {
|
|
129
133
|
return null;
|
|
130
134
|
}
|
|
131
|
-
const tinctures = [...classes].filter((cls) =>
|
|
135
|
+
const tinctures = [...classes].filter((cls) => PAINT.has(cls));
|
|
132
136
|
return tinctures.length === 1 ? tinctures[0] : null;
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
const isEmptyNode = (node) => {
|
|
140
|
+
switch (node.type) {
|
|
141
|
+
case "Comment": {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
case "JSXExpressionContainer": {
|
|
145
|
+
return node.expression.type === "JSXEmptyExpression";
|
|
146
|
+
}
|
|
147
|
+
case "Text":
|
|
148
|
+
case "JSXText": {
|
|
149
|
+
return node.value.trim() === "";
|
|
150
|
+
}
|
|
151
|
+
default: {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// A canton or inescutcheon with nothing on it. Cantons/inescutcheons with their
|
|
158
|
+
// own arms are exempt from rule-of-tincture.
|
|
159
|
+
export function isPlain(el) {
|
|
160
|
+
const { ordinaries, chiefs, bordures } = ordinariesOf(el.classes);
|
|
161
|
+
return (
|
|
162
|
+
fieldTincture(el.classes) !== null &&
|
|
163
|
+
!ordinaries.length &&
|
|
164
|
+
!chiefs.length &&
|
|
165
|
+
!bordures.length &&
|
|
166
|
+
(el.node.children ?? []).every(isEmptyNode)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// A field's first tincture: its bare tincture, or its division's first.
|
|
171
|
+
const fieldFirst = (classes) => {
|
|
172
|
+
const division = [...classes].find((cls) => DIVISION_CLASSES.has(cls));
|
|
173
|
+
return division ? DIVISION_CLASSES.get(division) : fieldTincture(classes);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// `of-the-first` on an element: the nearest escutcheon's or quarter's first (not a canton's).
|
|
177
|
+
// Null if the shield can't be seen.
|
|
178
|
+
export function firstOf(el) {
|
|
179
|
+
for (let node = el; node && node !== UNKNOWN; node = node.parent) {
|
|
180
|
+
if (node.classes?.has("escutcheon") || node.parent?.classes?.has("quarterly")) {
|
|
181
|
+
const first = fieldFirst(node.classes);
|
|
182
|
+
return first === OF_THE_FIRST ? null : first;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
135
188
|
// A charge's tincture, and the class that gives it, e.g. `bezant` → or.
|
|
136
189
|
export function chargeTincture(classes) {
|
|
137
190
|
const roundel = [...classes].find((cls) => ROUNDELS[cls]);
|
|
138
191
|
if (roundel) {
|
|
139
192
|
return { cls: roundel, tincture: ROUNDELS[roundel] };
|
|
140
193
|
}
|
|
141
|
-
const tinctures = [...classes].filter((cls) =>
|
|
194
|
+
const tinctures = [...classes].filter((cls) => PAINT.has(cls));
|
|
142
195
|
return tinctures.length === 1 ? { cls: tinctures[0], tincture: tinctures[0] } : null;
|
|
143
196
|
}
|
|
144
197
|
|
|
@@ -158,7 +211,11 @@ export const isCharge = (classes) => chargeName(classes) !== null;
|
|
|
158
211
|
|
|
159
212
|
export const pointOf = (classes) => [...classes].find((cls) => POINT_CLASSES.has(cls));
|
|
160
213
|
|
|
161
|
-
|
|
214
|
+
const atChiefPoint = (classes) => [...classes].some((cls) => CHIEF_POINTS.has(cls));
|
|
215
|
+
|
|
216
|
+
// A charge on the chief, if the shield has one: at a chief point, or arranged `in-chief`.
|
|
217
|
+
export const inChief = (container, charge) =>
|
|
218
|
+
atChiefPoint(charge.classes) || container.classes.has("in-chief");
|
|
162
219
|
|
|
163
220
|
export const arrangementsOf = (classes) => ALL_ARRANGEMENTS.filter((name) => classes.has(name));
|
|
164
221
|
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { getElements, UNKNOWN } from "../tree.js";
|
|
2
|
-
import { chargeName } from "../blazon.js";
|
|
2
|
+
import { arrangementsOf, chargeName } from "../blazon.js";
|
|
3
|
+
|
|
4
|
+
// The tail of the `arranged` message: where to place one charge instead, or "" for none.
|
|
5
|
+
const hint = (cls) => {
|
|
6
|
+
return (
|
|
7
|
+
{
|
|
8
|
+
"in-chief": "; to place one charge, use `in-chief-point`",
|
|
9
|
+
"in-base": "; to place one charge, use `in-base-point`",
|
|
10
|
+
}[cls] ?? ""
|
|
11
|
+
);
|
|
12
|
+
};
|
|
3
13
|
|
|
4
14
|
export default {
|
|
5
15
|
meta: {
|
|
6
16
|
type: "problem",
|
|
7
17
|
docs: {
|
|
8
|
-
description: "Require charges to be inside a `charges` container",
|
|
18
|
+
description: "Require charges to be inside a `charges` container, and arrangements on it",
|
|
9
19
|
url: "https://github.com/pafirmin/grailwind/tree/main/packages/eslint-plugin#charges-in-container",
|
|
10
20
|
},
|
|
11
21
|
schema: [],
|
|
12
22
|
messages: {
|
|
13
23
|
outside: "`{{charge}}` must be inside a `charges` container",
|
|
24
|
+
arranged: "`{{cls}}` arranges a `charges` container, not a charge{{hint}}",
|
|
14
25
|
},
|
|
15
26
|
},
|
|
16
27
|
|
|
@@ -21,10 +32,20 @@ export default {
|
|
|
21
32
|
if (!el.classes || el.parent === UNKNOWN || !el.parent.classes) {
|
|
22
33
|
continue;
|
|
23
34
|
}
|
|
35
|
+
const inContainer = el.parent.classes.has("charges");
|
|
24
36
|
const charge = chargeName(el.classes);
|
|
25
|
-
if (charge && !
|
|
37
|
+
if (charge && !inContainer) {
|
|
26
38
|
context.report({ node: el.reportNode, messageId: "outside", data: { charge } });
|
|
27
39
|
}
|
|
40
|
+
if (inContainer) {
|
|
41
|
+
for (const cls of arrangementsOf(el.classes)) {
|
|
42
|
+
context.report({
|
|
43
|
+
node: el.reportNode,
|
|
44
|
+
messageId: "arranged",
|
|
45
|
+
data: { cls, hint: hint(cls) },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
28
49
|
}
|
|
29
50
|
},
|
|
30
51
|
};
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { FURS } from "grailwind/data";
|
|
2
|
-
import { getElements } from "../tree.js";
|
|
1
|
+
import { FURS, OF_THE_FIRST } from "grailwind/data";
|
|
2
|
+
import { getElements, UNKNOWN } from "../tree.js";
|
|
3
3
|
import {
|
|
4
|
-
atChiefPoint,
|
|
5
4
|
chargeName,
|
|
6
5
|
chargeTincture,
|
|
7
6
|
describe,
|
|
8
7
|
fieldTincture,
|
|
9
8
|
fimbriationOf,
|
|
9
|
+
firstOf,
|
|
10
10
|
furKinds,
|
|
11
|
+
inChief,
|
|
11
12
|
isFimbriable,
|
|
12
13
|
isFur,
|
|
14
|
+
isPlain,
|
|
13
15
|
kind,
|
|
14
16
|
ordinariesOf,
|
|
15
17
|
sameKind,
|
|
@@ -64,22 +66,40 @@ export default {
|
|
|
64
66
|
messages: {
|
|
65
67
|
violation:
|
|
66
68
|
"`{{cls}}` puts {{thing}} {{tincture}} on {{ground}}: {{kind}} on {{kind}}{{note}}",
|
|
69
|
+
invisible:
|
|
70
|
+
"`{{cls}}` puts {{thing}} of the first on {{ground}}, its own tincture: it can't be seen",
|
|
71
|
+
plain:
|
|
72
|
+
"`{{cls}}` puts {{thing}} {{tincture}} on {{ground}}: {{kind}} on {{kind}}{{note}}. Change its tincture, or give it arms of its own",
|
|
67
73
|
},
|
|
68
74
|
},
|
|
69
75
|
|
|
70
76
|
create(context) {
|
|
71
77
|
const furs = furKinds(context.options[0]?.furs);
|
|
72
|
-
|
|
78
|
+
// `el` is the element whose field or ordinary it is, for resolving `of-the-first`.
|
|
79
|
+
const check = (el, node, cls, thing, painted, ground, messageId = "violation") => {
|
|
80
|
+
const first = [painted, ground].includes(OF_THE_FIRST) ? firstOf(el) : null;
|
|
81
|
+
const tincture = painted === OF_THE_FIRST ? first : painted;
|
|
82
|
+
if (ground === OF_THE_FIRST) {
|
|
83
|
+
ground = first;
|
|
84
|
+
}
|
|
85
|
+
if (painted === OF_THE_FIRST && tincture && tincture === ground) {
|
|
86
|
+
context.report({
|
|
87
|
+
node,
|
|
88
|
+
messageId: "invisible",
|
|
89
|
+
data: { cls, thing: article(thing), ground: titleCase(ground) },
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
73
93
|
if (!sameKind(tincture, ground, furs)) {
|
|
74
94
|
return;
|
|
75
95
|
}
|
|
76
96
|
context.report({
|
|
77
97
|
node,
|
|
78
|
-
messageId
|
|
98
|
+
messageId,
|
|
79
99
|
data: {
|
|
80
100
|
cls,
|
|
81
101
|
thing: article(thing),
|
|
82
|
-
tincture: titleCase(tincture),
|
|
102
|
+
tincture: titleCase(tincture) + (painted === OF_THE_FIRST ? " (of the first)" : ""),
|
|
83
103
|
ground: titleCase(ground),
|
|
84
104
|
kind: kind(tincture, furs),
|
|
85
105
|
note: furNote([ground, tincture], kind(tincture, furs)),
|
|
@@ -96,14 +116,22 @@ export default {
|
|
|
96
116
|
const field = fieldTincture(el.classes);
|
|
97
117
|
const { ordinaries, chiefs, bordures } = ordinariesOf(el.classes);
|
|
98
118
|
const fimbriation = fimbriationOf(el.classes);
|
|
119
|
+
|
|
120
|
+
// A plain sub-shield lies on its parent's field. A charged or divided one is arms.
|
|
121
|
+
const sub = ["canton", "inescutcheon"].find((c) => el.classes.has(c));
|
|
122
|
+
if (sub && isPlain(el) && el.parent !== UNKNOWN && el.parent.classes) {
|
|
123
|
+
const ground = fieldTincture(el.parent.classes);
|
|
124
|
+
check(el, el.reportNode, field, `plain ${sub}`, field, ground, "plain");
|
|
125
|
+
}
|
|
99
126
|
const isFimbriated = fimbriation && ordinaries.some(isFimbriable);
|
|
100
127
|
|
|
101
128
|
if (isFimbriated) {
|
|
102
|
-
check(el.reportNode, fimbriation.cls, "fimbriation", fimbriation.tincture, field);
|
|
129
|
+
check(el, el.reportNode, fimbriation.cls, "fimbriation", fimbriation.tincture, field);
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
for (const o of ordinaries) {
|
|
106
133
|
check(
|
|
134
|
+
el,
|
|
107
135
|
el.reportNode,
|
|
108
136
|
o.cls,
|
|
109
137
|
name(o),
|
|
@@ -114,7 +142,7 @@ export default {
|
|
|
114
142
|
|
|
115
143
|
// An ordinary abuts the chief rather than lying on it, so both are checked against the field.
|
|
116
144
|
for (const o of [...chiefs, ...bordures]) {
|
|
117
|
-
check(el.reportNode, o.cls, name(o), o.tincture, field);
|
|
145
|
+
check(el, el.reportNode, o.cls, name(o), o.tincture, field);
|
|
118
146
|
}
|
|
119
147
|
|
|
120
148
|
for (const container of el.children) {
|
|
@@ -137,12 +165,12 @@ export default {
|
|
|
137
165
|
let ground = field;
|
|
138
166
|
if (container.classes.has("on") && ordinaries.length) {
|
|
139
167
|
ground = ordinaries[0].tincture;
|
|
140
|
-
} else if (chiefs.length &&
|
|
168
|
+
} else if (chiefs.length && inChief(container, charge)) {
|
|
141
169
|
ground = chiefs[0].tincture;
|
|
142
170
|
}
|
|
143
171
|
|
|
144
172
|
const thing = describe(chargeName(charge.classes) ?? "charge");
|
|
145
|
-
check(charge.reportNode, found.cls, thing, found.tincture, ground);
|
|
173
|
+
check(el, charge.reportNode, found.cls, thing, found.tincture, ground);
|
|
146
174
|
}
|
|
147
175
|
}
|
|
148
176
|
}
|