@grailwind/eslint-plugin 0.1.0-beta.4 → 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 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.8.x. For typos and other class errors, see
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
- `escutcheon or fess-argent` is metal on metal. So are engrailed, invected, wavy, indented, embattled and voided ones
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 counts as arms of its own and is exempt.
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, and reported if it can't be seen.
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. Authorities differ, so the `furs` option changes this:
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 its ground.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grailwind/eslint-plugin",
3
- "version": "0.1.0-beta.4",
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.8.0"
29
+ "grailwind": "^0.9.0"
30
30
  },
31
31
  "peerDependenciesMeta": {
32
32
  "@html-eslint/parser": {
package/src/blazon.js CHANGED
@@ -4,9 +4,12 @@
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,
12
15
  lineOrdinaries,
@@ -38,7 +41,10 @@ const ORDINARY_CLASSES = new Map();
38
41
  for (const ordinary of ORDINARIES) {
39
42
  for (const tincture of PAINT) {
40
43
  const fur = FUR.has(tincture);
41
- ORDINARY_CLASSES.set(`${ordinary}-${tincture}`, { ordinary, tincture, fur });
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
+ }
42
48
  for (const line of LINES.filter((l) => lineOrdinaries(l).includes(ordinary))) {
43
49
  ORDINARY_CLASSES.set(`${ordinary}-${line}-${tincture}`, { ordinary, tincture, fur, line });
44
50
  }
@@ -66,8 +72,17 @@ for (const ordinary of VOIDABLE) {
66
72
  voided: true,
67
73
  });
68
74
  }
69
- for (const tincture of [...TINCTURES, OF_THE_FIRST]) {
70
- ORDINARY_CLASSES.set(`bordure-${tincture}`, { ordinary: "bordure", tincture });
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
+ }
71
86
  }
72
87
 
73
88
  // Each division class, mapped to its first tincture. Two furs in one division aren't generated.
@@ -87,19 +102,19 @@ export const isFimbriable = (o) => !o.counterchanged && !o.line && !o.fur && !o.
87
102
 
88
103
  export const isFur = (tincture) => FUR.has(tincture);
89
104
 
90
- const groundKind = (fur) => kind(FURS[fur][0]);
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");
91
107
 
92
108
  // What the rule's `furs` option makes of one fur: "metal", "colour" or "exempt". The rule's
93
109
  // schema has already checked the option.
94
110
  const furChoice = (fur, option) => {
95
111
  if (option === "ground") {
96
- return groundKind(fur);
112
+ return byDefault(fur);
97
113
  }
98
114
  if (option === "exempt") {
99
115
  return "exempt";
100
116
  }
101
- // A map: a fur left out counts by its ground.
102
- return option[fur] ?? groundKind(fur);
117
+ return option[fur] ?? byDefault(fur);
103
118
  };
104
119
 
105
120
  // Each fur's kind for kind() and sameKind(). Exempt is null, which they treat as "no check".
@@ -146,6 +161,36 @@ export function ordinariesOf(classes) {
146
161
 
147
162
  export const isDivided = (classes) => [...classes].some((cls) => DIVISION_CLASSES.has(cls));
148
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
+
149
194
  // The single bare tincture of a plain field. A divided field, or none, has no tincture.
150
195
  export function fieldTincture(classes) {
151
196
  if (isDivided(classes)) {
@@ -192,11 +237,12 @@ const fieldFirst = (classes) => {
192
237
  return division ? DIVISION_CLASSES.get(division) : fieldTincture(classes);
193
238
  };
194
239
 
195
- // `of-the-first` on an element: the nearest escutcheon's or quarter's first (not a canton's).
196
- // 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.
197
242
  export function firstOf(el) {
198
243
  for (let node = el; node && node !== UNKNOWN; node = node.parent) {
199
- if (node.classes?.has("escutcheon") || node.parent?.classes?.has("quarterly")) {
244
+ const frame = node.parent?.classes && frameOf(node.parent.classes);
245
+ if ((frame && frame !== "inescutcheon") || node.parent?.classes?.has("quarterly")) {
200
246
  const first = fieldFirst(node.classes);
201
247
  return first === OF_THE_FIRST ? null : first;
202
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
+ };
@@ -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
- if (sub && isPlain(el) && el.parent !== UNKNOWN && el.parent.classes) {
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
- check(el, el.reportNode, field, `plain ${sub}`, field, ground, "plain");
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 field, not a tincture of their own.
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
  }