@baravak/risloo-profile-cli 4.77.0 → 4.82.0

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.
@@ -24,6 +24,42 @@ Ask for the relevant inputs:
24
24
 
25
25
  Then **ask for the designer's Figma comments** — thresholds, coefficients, conditional behavior, spacing rules, and other implementation notes. The Figma MCP / SVG / HTML do **not** contain comments, and these specs usually live only there. Don't start coding until you have them.
26
26
 
27
+ ### Figma MCP budget discipline — fetch the whole artifact, not pieces
28
+
29
+ Figma MCP read calls are **rate limited per plan and seat**, and the cheap tiers are brutal (a View/Collab seat, or any seat on a Starter plan, gets **6 calls per month**; a Dev/Full seat on Professional gets 200/day, 10/min). Running out mid-task strands the work. So treat every read call as expensive and design for **maximum data per request**.
30
+
31
+ Rules:
32
+
33
+ 1. **Download the complete artifact set up front — source, SVG, and PNG — then work locally.** Do not drip-feed one layer per call. Prefer `download_assets` on the **Chart** node: one call returns the whole-node export plus the vector SVGs of its subtree. Add `get_metadata` on the same node for the layer tree with exact `x/y/width/height`. Those two calls plus one `get_screenshot` give you geometry, vectors, and the visual reference for a whole page.
34
+ 2. **Asset URLs cost nothing.** `get_design_context` on a vector-heavy layer returns an `<img src="…svg">` asset URL instead of code, and `get_screenshot` returns a PNG URL. Downloading those with `curl` does **not** consume quota. Always pull the file and read it locally rather than making another MCP call.
35
+ 3. **Never call `get_design_context` on a repeated component instance more than once.** A dotted-column or dash-ruler instance can expand to hundreds of nodes, burning a call and flooding context. Sample **one** atom for its style, then derive the repetition from `get_metadata` sizes and the pitch arithmetic.
36
+ 4. **Answer remaining questions from the downloaded PNG/SVG, not from Figma.** Exact colors, tick pitch, mirroring, and alignment are all measurable locally (e.g. sample pixels with Python/PIL, grep the SVG for `stroke=`/`fill=`). Go back to MCP only for text content and semantics that the outlined SVG genuinely cannot carry.
37
+ 5. **Batch the calls you do need in one message** so they run in parallel and stay inside the per-minute cap; keep a batch at or below the seat's per-minute limit.
38
+ 6. **Check the budget before a big pull.** `whoami` is exempt from rate limits and reports every plan and seat — use it when a call fails or before planning a large fetch. On a limit error, stop and tell the user which seat/plan is capping, rather than retrying.
39
+ 7. **If the quota is exhausted, ask the user to export the files** (Chart SVG + PNG) instead of waiting — that path costs zero calls and the SVG is authoritative for color and geometry anyway.
40
+
41
+ #### Access tiers and which tools are metered
42
+
43
+ | Seat | Starter | Professional | Organization | Enterprise |
44
+ |---|---|---|---|---|
45
+ | View, Collab | 6 / month | 6 / month | 6 / month | 6 / month |
46
+ | Dev, Full | 6 / month | 200 / day, 10 / min | 200 / day, 15 / min | 600 / day, 20 / min |
47
+
48
+ - **Metered:** every tool that *reads* from Figma — `get_design_context`, `get_metadata`, `get_screenshot`, `get_variable_defs`, `download_assets`, and the rest.
49
+ - **Exempt:** `whoami`, `generate_figma_design`, `add_code_connect_map`, and other write-to-Figma tools. Reading MCP **resources** (`skill://…`, `file://figma/docs/…`) is also not metered.
50
+ - Quota follows the **plan that owns the file**, not the user's best seat. A user with a Full seat on their own Starter team and a View seat on the team that owns the design still gets 6/month for that design. `whoami` returns every plan with its `tier` and `seat`, plus the plan key — the plan key also appears in the rate-limit error URL, which identifies exactly which plan is capping.
51
+ - The fix is usually a **seat** upgrade (View → Dev/Full) on the owning team, not a plan upgrade. Report that distinction to the user instead of suggesting they buy a bigger plan.
52
+
53
+ ### Recommended one-shot fetch sequence for a new page
54
+
55
+ For each Chart node, in a single batched message:
56
+
57
+ 1. `get_metadata` — the layer tree with exact `x/y/width/height` for every node. This is the geometry ledger's backbone.
58
+ 2. `download_assets` with `defaultFormat: "svg"` — returns a URL for the whole-node vector export plus the subtree's SVG assets.
59
+ 3. `get_screenshot` at a `maxDimension` at least equal to the Chart's natural width — the visual acceptance reference.
60
+
61
+ Then `curl` all returned URLs (free) and do the rest locally: read the export SVG for exact colors, strokes, radii, gradients and paint order; read the metadata for layout boxes and text-node dimensions; compare renders against the PNG. Only text **content** needs a further call, and `get_metadata` already carries it in the layer `name` for text nodes.
62
+
27
63
  Do not select one design artifact and ignore the others. Establish an authority table for the current task:
28
64
 
29
65
  - **PNG** — final visual acceptance target, including visible orientation, wrapping, cropping, alignment, and composition.
@@ -0,0 +1,70 @@
1
+ name: Publish to npm
2
+
3
+ # Releases are driven by a tag:
4
+ #
5
+ # npm version <patch|minor|major>
6
+ # git push --follow-tags
7
+ #
8
+ # No npm token is involved. The job authenticates to the registry with a
9
+ # short-lived OIDC credential issued to this workflow (trusted publishing),
10
+ # which is what `id-token: write` below grants. The registry must be told to
11
+ # trust this repository + workflow filename in the package's settings.
12
+ on:
13
+ push:
14
+ tags:
15
+ - "v*"
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+ id-token: write
21
+
22
+ jobs:
23
+ publish:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v7
27
+
28
+ - uses: actions/setup-node@v7
29
+ with:
30
+ node-version: 24
31
+ registry-url: https://registry.npmjs.org
32
+ cache: npm
33
+
34
+ # Trusted publishing needs npm 11.5.1+. The npm bundled with Node usually
35
+ # already satisfies that, so only reach for the network when it does not
36
+ # — an unconditional global install is a needless failure point.
37
+ - name: Ensure npm supports trusted publishing
38
+ run: |
39
+ need=11.5.1
40
+ have="$(npm --version)"
41
+ if [ "$(printf '%s\n%s\n' "$need" "$have" | sort -V | head -n1)" = "$need" ]; then
42
+ echo "npm $have satisfies >= $need"
43
+ else
44
+ echo "npm $have is older than $need, upgrading"
45
+ for attempt in 1 2 3; do
46
+ npm install -g npm@latest && break
47
+ echo "attempt $attempt failed, retrying"; sleep 10
48
+ done
49
+ fi
50
+ npm --version
51
+
52
+ - run: npm ci --fetch-retries=5
53
+
54
+ # sharp carries a native binary. Fail here with a clear message rather
55
+ # than part-way through the release.
56
+ - name: Smoke-check the native image pipeline
57
+ run: node -e "require('sharp'); console.log('sharp loaded')"
58
+
59
+ # A tag that disagrees with package.json would publish the wrong version.
60
+ - name: Check the tag matches package.json
61
+ if: github.event_name == 'push'
62
+ run: |
63
+ tag="${GITHUB_REF_NAME#v}"
64
+ pkg="$(node -p "require('./package.json').version")"
65
+ echo "tag=$tag package.json=$pkg"
66
+ test "$tag" = "$pkg"
67
+
68
+ # Runs prepublishOnly (npm test) first. Provenance is attached
69
+ # automatically because this is a public repository.
70
+ - run: npm publish
@@ -1,14 +1,23 @@
1
- name: push-package
1
+ name: CI
2
+
2
3
  on:
3
- push
4
+ push:
5
+ pull_request:
6
+
4
7
  jobs:
5
- build:
8
+ test:
6
9
  runs-on: ubuntu-latest
7
10
  steps:
8
- - uses: actions/checkout@v2
9
- - uses: actions/setup-node@v2
11
+ - uses: actions/checkout@v7
12
+
13
+ - uses: actions/setup-node@v7
10
14
  with:
11
- node-version: "16.x"
12
- - run: |
13
- yarn
14
- yarn test
15
+ node-version: 24
16
+ cache: npm
17
+
18
+ # `npm ci` installs exactly what package-lock.json pins; the previous
19
+ # `yarn` step ignored that lockfile.
20
+ - run: npm ci
21
+
22
+ # Renders every sample in both variants.
23
+ - run: npm test
package/.mcp.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "figma": {
4
+ "type": "http",
5
+ "url": "https://mcp.figma.com/mcp"
6
+ }
7
+ }
8
+ }
package/AGENTS.md CHANGED
@@ -44,8 +44,7 @@ risloo-extractor-app/
44
44
  │ └── publish/
45
45
  │ ├── json/profiles/ # Template JSON per sample
46
46
  │ ├── json/gift/ # Gift template data
47
- ├── test.js # Auto-test all samples
48
- │ └── bot.js # Post-publish automation
47
+ └── test.js # Auto-test all samples
49
48
  ├── views/
50
49
  │ ├── profiles/samples/ # Handlebars SVG templates (.hbs)
51
50
  │ └── gift.hbs
@@ -387,7 +386,7 @@ If several local coordinate patches accumulate, stop. Re-establish the coordinat
387
386
  | Dates | Moment.js + moment-jalaali (Persian calendar) |
388
387
  | QR codes | qrcode |
389
388
  | File watching | Chokidar |
390
- | Package manager | Yarn |
389
+ | Package manager | npm (`package-lock.json` is the lockfile) |
391
390
  | Design source | Figma (via MCP) |
392
391
 
393
392
  ---
@@ -405,9 +404,17 @@ If several local coordinate patches accumulate, stop. Re-establish the coordinat
405
404
 
406
405
  ## Publishing
407
406
 
407
+ Releases go through GitHub Actions via npm **trusted publishing** (OIDC) — there
408
+ is no npm token anywhere, local or in secrets. Tag the release and push it; the
409
+ `.github/workflows/publish.yml` job publishes it.
410
+
408
411
  ```bash
409
- npm version <patch|minor|major>
410
- npm publish
411
- # prepublishOnly: npm test
412
- # postpublish: npm run bot
412
+ npm version <patch|minor|major> # bumps package.json and creates the vX.Y.Z tag
413
+ git push --follow-tags # pushing the tag triggers the release
414
+ # prepublishOnly: npm test — runs inside the job before the registry is touched
413
415
  ```
416
+
417
+ The job refuses to publish if the tag and `package.json` version disagree.
418
+ Publishing from a laptop with `npm publish` is not expected to work: npm
419
+ removed non-expiring tokens, and the package is configured to trust this
420
+ workflow instead.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baravak/risloo-profile-cli",
3
- "version": "4.77.0",
3
+ "version": "4.82.0",
4
4
  "description": "**Risloo Profile CLI** is a library for creating profiles, reports and sheets for *psychological* samples.",
5
5
  "main": "bin/risloo.js",
6
6
  "publishConfig": {
@@ -10,10 +10,8 @@
10
10
  "risloo": "bin/risloo.js"
11
11
  },
12
12
  "scripts": {
13
- "bot": "node ./src/publish/bot.js",
14
13
  "test": "node ./src/publish/test.js",
15
14
  "prepublishOnly": "npm test",
16
- "postpublish": "npm run bot",
17
15
  "start": "node app"
18
16
  },
19
17
  "repository": {
@@ -26,8 +24,6 @@
26
24
  "chalk": "^4.1.2",
27
25
  "chokidar": "^3.5.3",
28
26
  "commander": "^8.3.0",
29
- "dotenv": "^16.0.0",
30
- "form-data": "^4.0.0",
31
27
  "handlebars": "^4.7.7",
32
28
  "moment": "^2.29.1",
33
29
  "moment-jalaali": "^0.9.2",
@@ -40,4 +36,4 @@
40
36
  "url": "https://github.com/baravak/risloo-extractor-app/issues"
41
37
  },
42
38
  "homepage": "https://github.com/baravak/risloo-extractor-app#readme"
43
- }
39
+ }
@@ -58,14 +58,44 @@ class BSSI93 extends Profile {
58
58
  const end = FS.toRadians(180); // 100% at left (9 o'clock)
59
59
  total.zeta = total.p * (end - start) + start; // clockwise 270° sweep
60
60
 
61
- // --- Factors (vertical bars, left → right) ---
62
- // Domain names are drawn under each bar; 2-line ones split as in the design.
63
- const lines = [["تمایل به مرگ"], ["آمادگی برای", "خودکشی"], ["تمایل به", "خودکشی واقعی"]];
64
- const factors = [packItem(s[2], s[3]), packItem(s[4], s[5]), packItem(s[6], s[7])].map((f, i) => ({
65
- ...f,
66
- cx: 120 + i * 100, // bar centres: 120, 220, 320
67
- trackLeft: 106 + i * 100, // track/bar left edges: 106, 206, 306
68
- lines: lines[i],
61
+ // --- Factors (visual order, left → right) ---
62
+ // The scientific score keys stay unchanged. The design places the intent
63
+ // score in the middle slot and the preparation score in the right slot.
64
+ const factorSpecs = [
65
+ {
66
+ raw: s[2],
67
+ percentage: s[3],
68
+ cx: 120,
69
+ trackLeft: 106,
70
+ scoreX: 120.5,
71
+ lines: [{ text: "تمایل به مرگ", x: 120, y: 351.5 }],
72
+ },
73
+ {
74
+ raw: s[6],
75
+ percentage: s[7],
76
+ cx: 220,
77
+ trackLeft: 206,
78
+ scoreX: 221.5,
79
+ lines: [
80
+ { text: "آمادگی برای", x: 219.5, y: 351.5 },
81
+ { text: "خودکشی", x: 219, y: 368 },
82
+ ],
83
+ },
84
+ {
85
+ raw: s[4],
86
+ percentage: s[5],
87
+ cx: 320,
88
+ trackLeft: 306,
89
+ scoreX: 322,
90
+ lines: [
91
+ { text: "تمایل به", x: 322, y: 352.5 },
92
+ { text: "خودکشی واقعی", x: 322, y: 368 },
93
+ ],
94
+ },
95
+ ];
96
+ const factors = factorSpecs.map(({ raw, percentage, ...geometry }) => ({
97
+ ...packItem(raw, percentage),
98
+ ...geometry,
69
99
  }));
70
100
 
71
101
  return [{ total, factors }];
@@ -52,12 +52,55 @@ for (const { key } of DOMAINS) {
52
52
  }
53
53
  }
54
54
 
55
+ // Page 4 (profile sheet). The Chart layer is 885 × 649; every number below is
56
+ // measured from it, so the shared NEO_sheet partial stays free of profile logic.
57
+ const SHEET_GROUP_X = [54, 175, 317, 459, 601, 743];
58
+ const SHEET_COL_STEP = 21;
59
+ const SHEET_COL_HALF = 6.5;
60
+
61
+ // 0 % sits on the bottom gridline, 100 % on the top one, 500 px apart.
62
+ const SHEET_ZERO_Y = 632;
63
+ const SHEET_PX_PER_PERCENT = 5;
64
+
65
+ const SHEET_DOMAIN_STROKE = "#4338CA";
66
+ const SHEET_FACET_STROKE = "#334155";
67
+
55
68
  function clamp(value, min, max) {
56
69
  const number = Number(value);
57
70
  if (!Number.isFinite(number)) return min;
58
71
  return Math.min(max, Math.max(min, number));
59
72
  }
60
73
 
74
+ function sheetColumns() {
75
+ const columns = DOMAINS.map(({ fa, letter }, index) => ({
76
+ cx: SHEET_GROUP_X[0] + SHEET_COL_HALF + index * SHEET_COL_STEP,
77
+ code: letter,
78
+ fa,
79
+ domain: true,
80
+ }));
81
+
82
+ DOMAINS.forEach(({ key, letter }, groupIndex) => {
83
+ FACET_NAMES[key].forEach((fa, index) => {
84
+ columns.push({
85
+ cx: SHEET_GROUP_X[groupIndex + 1] + SHEET_COL_HALF + index * SHEET_COL_STEP,
86
+ code: `${letter}${index + 1}`,
87
+ fa,
88
+ domain: false,
89
+ });
90
+ });
91
+ });
92
+
93
+ return columns;
94
+ }
95
+
96
+ // One dash per axis unit; every `accentEvery`-th one is the dark, readable tick.
97
+ function sheetDashes(top, pitch, count, accentEvery) {
98
+ return Array.from({ length: count }, (unused, index) => ({
99
+ y: top + index * pitch,
100
+ dark: index % accentEvery === 0,
101
+ }));
102
+ }
103
+
61
104
  function layoutIndicators(indicators) {
62
105
  let start = 0;
63
106
 
@@ -73,11 +116,12 @@ function layoutIndicators(indicators) {
73
116
  }
74
117
 
75
118
  class NEO93 extends Profile {
76
- static pages = 3;
119
+ static pages = 4;
77
120
 
78
121
  static partials = {
79
122
  NEO_main: "NEO_main.hbs",
80
123
  NEO_long_facets: "NEO_long_facets.hbs",
124
+ NEO_sheet: "NEO_sheet.hbs",
81
125
  };
82
126
 
83
127
  labels = {
@@ -99,26 +143,35 @@ class NEO93 extends Profile {
99
143
  },
100
144
  profile: {
101
145
  get dimensions() {
102
- const [page1, page2, page3] = this.padding;
146
+ const [page1, page2, page3, page4] = this.padding;
103
147
  return [
104
148
  {
105
- width: 800 + 2 * page1.x,
106
- height: 674 + 2 * page1.y,
149
+ width: 885 + 2 * page1.x,
150
+ height: 649 + 2 * page1.y,
107
151
  },
108
152
  {
109
- width: 811 + 2 * page2.x,
110
- height: 662 + 2 * page2.y,
153
+ width: 800 + 2 * page2.x,
154
+ height: 674 + 2 * page2.y,
111
155
  },
112
156
  {
113
157
  width: 811 + 2 * page3.x,
114
- height: 458 + 2 * page3.y,
158
+ height: 662 + 2 * page3.y,
159
+ },
160
+ {
161
+ width: 811 + 2 * page4.x,
162
+ height: 458 + 2 * page4.y,
115
163
  },
116
164
  ];
117
165
  },
166
+ // Padding is the Chart's inset inside the 943 × 754 design page *minus*
167
+ // the 20 px the layout already owns on every side. Every page therefore
168
+ // resolves to 903 × 714 — the exact with-sidebar drawing area — and
169
+ // renders at scale 1 instead of being shrunk to fit.
118
170
  padding: [
119
- { x: 71.5, y: 40 },
120
- { x: 66, y: 46 },
121
- { x: 66, y: 148 },
171
+ { x: 9, y: 32.5 },
172
+ { x: 51.5, y: 20 },
173
+ { x: 46, y: 26 },
174
+ { x: 46, y: 128 },
122
175
  ],
123
176
  },
124
177
  labels: Object.values(this.labels),
@@ -247,11 +300,77 @@ class NEO93 extends Profile {
247
300
  };
248
301
  });
249
302
 
303
+ const sheetCols = sheetColumns();
304
+ const sheetY = (percentage) => SHEET_ZERO_Y - SHEET_PX_PER_PERCENT * clamp(percentage, 0, 100);
305
+ const polyline = (cols, values) => values.map((value, index) => `${cols[index].cx},${sheetY(value)}`).join(" ");
306
+
307
+ const series = [
308
+ { stroke: SHEET_DOMAIN_STROKE, points: polyline(sheetCols.slice(0, 5), items.map((item) => item.percentage)) },
309
+ ...blocks.map((block, groupIndex) => ({
310
+ stroke: SHEET_FACET_STROKE,
311
+ points: polyline(
312
+ sheetCols.slice(5 + groupIndex * 6, 11 + groupIndex * 6),
313
+ block.facets.map((facet) => facet.percentage)
314
+ ),
315
+ })),
316
+ ];
317
+
318
+ // The profile sheet opens the report; the bar pages follow it.
250
319
  return [
251
320
  {
252
321
  ...sharedContext,
253
322
  page: 1,
254
- titleAppend: titleAppend(1),
323
+ titleAppend: " - کلاسیک",
324
+ columns: sheetCols,
325
+ series,
326
+ sheet: {
327
+ grid: {
328
+ x: 31,
329
+ w: 854,
330
+ ys: [
331
+ { y: 131, faint: false },
332
+ { y: 231, faint: true },
333
+ { y: 331, faint: true },
334
+ { y: 431, faint: true },
335
+ { y: 531, faint: true },
336
+ { y: 631, faint: false },
337
+ ],
338
+ },
339
+ plot: { top: 132, bottom: 632 },
340
+ dot: { pitch: 5, accentPitch: 20, light: "#F1F5F9", accent: "#94A3B8" },
341
+ tick: { top: 121, h: 10 },
342
+ rail: {
343
+ dashLeftX: 36,
344
+ dashRightX: 873,
345
+ w: 6,
346
+ ruleLeftX: 45.5,
347
+ ruleRightX: 868.5,
348
+ ruleW: 1,
349
+ ruleTop: 131,
350
+ ruleH: 502,
351
+ dark: "#64748B",
352
+ light: "white",
353
+ },
354
+ dashes: sheetDashes(132, 5, 101, 4),
355
+ numberX: 24,
356
+ numbers: [
357
+ { y: 132, text: "100 ٪" },
358
+ { y: 232, text: "80 ٪" },
359
+ { y: 332, text: "60 ٪" },
360
+ { y: 432, text: "40 ٪" },
361
+ { y: 532, text: "20 ٪" },
362
+ { y: 632, text: "0" },
363
+ ],
364
+ levelX: 0,
365
+ levels: [],
366
+ titleBottomY: 115,
367
+ codeY: 648,
368
+ },
369
+ },
370
+ {
371
+ ...sharedContext,
372
+ page: 2,
373
+ titleAppend: titleAppend(2),
255
374
  items,
256
375
  redErrors: layoutIndicators(redErrors),
257
376
  yellowErrors: layoutIndicators(yellowErrors),
@@ -260,15 +379,15 @@ class NEO93 extends Profile {
260
379
  },
261
380
  {
262
381
  ...sharedContext,
263
- page: 2,
264
- titleAppend: titleAppend(2),
382
+ page: 3,
383
+ titleAppend: titleAppend(3),
265
384
  blocks: blocks.slice(0, 3),
266
385
  gridBottom: 658.5,
267
386
  },
268
387
  {
269
388
  ...sharedContext,
270
- page: 3,
271
- titleAppend: titleAppend(3),
389
+ page: 4,
390
+ titleAppend: titleAppend(4),
272
391
  blocks: blocks.slice(3, 5),
273
392
  gridBottom: 454.5,
274
393
  },
@@ -66,9 +66,13 @@ class NEO9A extends Profile {
66
66
  height: 674 + 2 * this.padding.y,
67
67
  };
68
68
  },
69
+ // Padding is the Chart's inset inside the 943 × 754 design page *minus*
70
+ // the 20 px the layout already owns on every side, so the page resolves
71
+ // to 903 × 714 — the exact with-sidebar drawing area — and renders at
72
+ // scale 1 instead of being shrunk to fit.
69
73
  padding: {
70
- x: 71.5,
71
- y: 40,
74
+ x: 51.5,
75
+ y: 20,
72
76
  },
73
77
  },
74
78
  labels: Object.values(this.labels),
@@ -52,12 +52,60 @@ for (const { key } of DOMAINS) {
52
52
  }
53
53
  }
54
54
 
55
+ // Page 4 (profile sheet). The Chart layer is 878 × 704; every number below is
56
+ // measured from it, so the shared NEO_sheet partial stays free of profile logic.
57
+ const SHEET_GROUP_X = [47, 168, 310, 452, 594, 736];
58
+ const SHEET_COL_STEP = 21;
59
+ const SHEET_COL_HALF = 6.5;
60
+
61
+ // The T axis runs 20…80 at a flat 9 px per point: T 20 on the bottom gridline,
62
+ // T 80 on the top one. `_lookup` floors at 20 and the norm tables top out at 80,
63
+ // but the clamp also keeps a missing key (which `?? 0` turns into 0) on the sheet.
64
+ const SHEET_MIN_T = 20;
65
+ const SHEET_MAX_T = 80;
66
+ const SHEET_MIN_T_Y = 687;
67
+ const SHEET_PX_PER_T = 9;
68
+
69
+ const SHEET_DOMAIN_STROKE = "#4338CA";
70
+ const SHEET_FACET_STROKE = "#334155";
71
+
55
72
  function clamp(value, min, max) {
56
73
  const number = Number(value);
57
74
  if (!Number.isFinite(number)) return min;
58
75
  return Math.min(max, Math.max(min, number));
59
76
  }
60
77
 
78
+ function sheetColumns() {
79
+ const columns = DOMAINS.map(({ fa, letter }, index) => ({
80
+ cx: SHEET_GROUP_X[0] + SHEET_COL_HALF + index * SHEET_COL_STEP,
81
+ code: letter,
82
+ fa,
83
+ domain: true,
84
+ }));
85
+
86
+ DOMAINS.forEach(({ key, letter }, groupIndex) => {
87
+ FACET_NAMES[key].forEach((fa, index) => {
88
+ columns.push({
89
+ cx: SHEET_GROUP_X[groupIndex + 1] + SHEET_COL_HALF + index * SHEET_COL_STEP,
90
+ code: `${letter}${index + 1}`,
91
+ fa,
92
+ domain: false,
93
+ });
94
+ });
95
+ });
96
+
97
+ return columns;
98
+ }
99
+
100
+ // One dash per T point; every `accentEvery`-th one is dark — on this form that
101
+ // marks the even T values, which are the only ones the norm tables can produce.
102
+ function sheetDashes(top, pitch, count, accentEvery) {
103
+ return Array.from({ length: count }, (unused, index) => ({
104
+ y: top + index * pitch,
105
+ dark: index % accentEvery === 0,
106
+ }));
107
+ }
108
+
61
109
  function layoutIndicators(indicators) {
62
110
  let start = 0;
63
111
 
@@ -73,11 +121,12 @@ function layoutIndicators(indicators) {
73
121
  }
74
122
 
75
123
  class NEO9Q extends Profile {
76
- static pages = 3;
124
+ static pages = 4;
77
125
 
78
126
  static partials = {
79
127
  NEO_main: "NEO_main.hbs",
80
128
  NEO_long_facets: "NEO_long_facets.hbs",
129
+ NEO_sheet: "NEO_sheet.hbs",
81
130
  };
82
131
 
83
132
  labels = {
@@ -99,26 +148,35 @@ class NEO9Q extends Profile {
99
148
  },
100
149
  profile: {
101
150
  get dimensions() {
102
- const [page1, page2, page3] = this.padding;
151
+ const [page1, page2, page3, page4] = this.padding;
103
152
  return [
104
153
  {
105
- width: 800 + 2 * page1.x,
106
- height: 674 + 2 * page1.y,
154
+ width: 878 + 2 * page1.x,
155
+ height: 704 + 2 * page1.y,
107
156
  },
108
157
  {
109
- width: 811 + 2 * page2.x,
110
- height: 668 + 2 * page2.y,
158
+ width: 800 + 2 * page2.x,
159
+ height: 674 + 2 * page2.y,
111
160
  },
112
161
  {
113
162
  width: 811 + 2 * page3.x,
114
- height: 464 + 2 * page3.y,
163
+ height: 668 + 2 * page3.y,
164
+ },
165
+ {
166
+ width: 811 + 2 * page4.x,
167
+ height: 464 + 2 * page4.y,
115
168
  },
116
169
  ];
117
170
  },
171
+ // Padding is the Chart's inset inside the 943 × 754 design page *minus*
172
+ // the 20 px the layout already owns on every side. Every page therefore
173
+ // resolves to 903 × 714 — the exact with-sidebar drawing area — and
174
+ // renders at scale 1 instead of being shrunk to fit.
118
175
  padding: [
119
- { x: 71.5, y: 40 },
120
- { x: 66, y: 43 },
121
- { x: 66, y: 145 },
176
+ { x: 12.5, y: 5 },
177
+ { x: 51.5, y: 20 },
178
+ { x: 46, y: 23 },
179
+ { x: 46, y: 125 },
122
180
  ],
123
181
  },
124
182
  labels: Object.values(this.labels),
@@ -249,11 +307,84 @@ class NEO9Q extends Profile {
249
307
  };
250
308
  });
251
309
 
310
+ const sheetCols = sheetColumns();
311
+ const sheetY = (t) => SHEET_MIN_T_Y - SHEET_PX_PER_T * (clamp(t, SHEET_MIN_T, SHEET_MAX_T) - SHEET_MIN_T);
312
+ const polyline = (cols, values) => values.map((value, index) => `${cols[index].cx},${sheetY(value)}`).join(" ");
313
+
314
+ const series = [
315
+ { stroke: SHEET_DOMAIN_STROKE, points: polyline(sheetCols.slice(0, 5), items.map((item) => item.tScore)) },
316
+ ...blocks.map((block, groupIndex) => ({
317
+ stroke: SHEET_FACET_STROKE,
318
+ points: polyline(
319
+ sheetCols.slice(5 + groupIndex * 6, 11 + groupIndex * 6),
320
+ block.facets.map((facet) => facet.tScore)
321
+ ),
322
+ })),
323
+ ];
324
+
325
+ // The profile sheet opens the report; the bar pages follow it.
252
326
  return [
253
327
  {
254
328
  ...sharedContext,
255
329
  page: 1,
256
- titleAppend: titleAppend(1),
330
+ titleAppend: " - کلاسیک",
331
+ columns: sheetCols,
332
+ series,
333
+ sheet: {
334
+ grid: {
335
+ x: 24,
336
+ w: 854,
337
+ ys: [
338
+ { y: 146, faint: false },
339
+ { y: 281, faint: true },
340
+ { y: 371, faint: true },
341
+ { y: 461, faint: true },
342
+ { y: 551, faint: true },
343
+ { y: 686, faint: false },
344
+ ],
345
+ },
346
+ plot: { top: 147, bottom: 687 },
347
+ dot: { pitch: 9, accentPitch: 18, light: "#F1F5F9", accent: "#94A3B8" },
348
+ tick: { top: 136, h: 10 },
349
+ rail: {
350
+ dashLeftX: 29,
351
+ dashRightX: 866,
352
+ w: 6,
353
+ ruleLeftX: 38,
354
+ ruleRightX: 861,
355
+ ruleW: 2,
356
+ ruleTop: 146,
357
+ ruleH: 542,
358
+ dark: "#64748B",
359
+ light: "#CBD5E1",
360
+ },
361
+ dashes: sheetDashes(147, 9, 61, 2),
362
+ numberX: 17,
363
+ numbers: [
364
+ { y: 147, text: "80" },
365
+ { y: 282, text: "65" },
366
+ { y: 372, text: "55" },
367
+ { y: 462, text: "45" },
368
+ { y: 552, text: "35" },
369
+ { y: 687, text: "20" },
370
+ ],
371
+ levelX: 9.9,
372
+ // Each band label sits midway between the two gridlines that bound it.
373
+ levels: [
374
+ { y: 214.5, text: LEVELS[5] },
375
+ { y: 327, text: LEVELS[4] },
376
+ { y: 417, text: LEVELS[3] },
377
+ { y: 507, text: LEVELS[2] },
378
+ { y: 619.5, text: LEVELS[1] },
379
+ ],
380
+ titleBottomY: 130,
381
+ codeY: 703,
382
+ },
383
+ },
384
+ {
385
+ ...sharedContext,
386
+ page: 2,
387
+ titleAppend: titleAppend(2),
257
388
  items,
258
389
  redErrors: layoutIndicators(redErrors),
259
390
  yellowErrors: layoutIndicators(yellowErrors),
@@ -262,15 +393,15 @@ class NEO9Q extends Profile {
262
393
  },
263
394
  {
264
395
  ...sharedContext,
265
- page: 2,
266
- titleAppend: titleAppend(2),
396
+ page: 3,
397
+ titleAppend: titleAppend(3),
267
398
  blocks: blocks.slice(0, 3),
268
399
  gridBottom: 664.5,
269
400
  },
270
401
  {
271
402
  ...sharedContext,
272
- page: 3,
273
- titleAppend: titleAppend(3),
403
+ page: 4,
404
+ titleAppend: titleAppend(4),
274
405
  blocks: blocks.slice(3, 5),
275
406
  gridBottom: 460.5,
276
407
  },
@@ -132,9 +132,13 @@ class NEO9V extends Profile {
132
132
  },
133
133
  ];
134
134
  },
135
+ // Padding is the Chart's inset inside the 943 × 754 design page *minus*
136
+ // the 20 px the layout already owns on every side. Both pages therefore
137
+ // resolve to 903 × 714 — the exact with-sidebar drawing area — and
138
+ // render at scale 1 instead of being shrunk to fit.
135
139
  padding: [
136
- { x: 71, y: 40 },
137
- { x: 91.5, y: 46 },
140
+ { x: 51, y: 20 },
141
+ { x: 71.5, y: 26 },
138
142
  ],
139
143
  },
140
144
  labels: Object.values(this.labels),
@@ -0,0 +1,70 @@
1
+ {{!--
2
+ NEO profile sheet (نیم‌رخ): 35 dotted columns — the 5 domains, then 6 groups of
3
+ 6 facets — over a horizontal gridline scale, with rotated titles above, latin
4
+ codes below, and one polyline per group.
5
+
6
+ Every coordinate arrives from the controller; nothing is derived here, so the
7
+ percentage sheet and the T-score sheet share this file without a variant flag.
8
+
9
+ padding {x, y} page padding owned by the layout
10
+ sheet.grid {x, w, ys: [{y, faint}]} gridline rects, y = rect top edge
11
+ sheet.plot {top, bottom} dot-column endpoints (gridline centres)
12
+ sheet.dot {pitch, accentPitch, light, accent}
13
+ sheet.tick {top, h} column ticks above the top gridline
14
+ sheet.rail {dashLeftX, dashRightX, w, ruleLeftX, ruleRightX, ruleW,
15
+ ruleTop, ruleH, dark, light, dashes: [{y, dark}]}
16
+ sheet.numberX right edge the axis numbers align to
17
+ sheet.numbers [{y, text}] axis numbers, y = gridline centre
18
+ sheet.levelX centre guide of the rotated level words
19
+ sheet.levels [{y, text}] rotated level words, may be empty
20
+ sheet.titleBottomY guide the rotated titles grow upward from
21
+ sheet.codeY baseline of the latin codes
22
+ columns [{cx, code, fa, domain}] 35 entries, design draw order
23
+ series [{points, stroke}] one polyline per group
24
+
25
+ Paint order mirrors the design export: codes, titles, dots, rails, numbers,
26
+ ticks, gridlines, result lines.
27
+ --}}
28
+ <g transform="translate({{padding.x}}, {{padding.y}})">
29
+ {{#each columns as |col|}}
30
+ <text class="eng-font" x="{{col.cx}}" y="{{../sheet.codeY}}" direction="ltr" font-size="10" font-weight="400" fill="#94A3B8" text-anchor="middle">{{col.code}}</text>
31
+ {{/each}}
32
+
33
+ {{#each columns as |col|}}
34
+ <text x="{{col.cx}}" y="{{../sheet.titleBottomY}}" transform="rotate(-90 {{col.cx}} {{../sheet.titleBottomY}})" direction="ltr" font-size="12" font-weight="{{ternary col.domain 600 400}}" fill="{{ternary col.domain '#4338CA' '#334155'}}" text-anchor="start" dy=".35em"><tspan class="eng-font">{{col.code}}</tspan> - {{col.fa}}</text>
35
+ {{/each}}
36
+
37
+ {{#each columns as |col|}}
38
+ <line x1="{{col.cx}}" y1="{{../sheet.plot.top}}" x2="{{col.cx}}" y2="{{../sheet.plot.bottom}}" stroke="{{../sheet.dot.light}}" stroke-width="2" stroke-linecap="round" stroke-dasharray="0 {{../sheet.dot.pitch}}"/>
39
+ <line x1="{{col.cx}}" y1="{{../sheet.plot.top}}" x2="{{col.cx}}" y2="{{../sheet.plot.bottom}}" stroke="{{../sheet.dot.accent}}" stroke-width="2" stroke-linecap="round" stroke-dasharray="0 {{../sheet.dot.accentPitch}}"/>
40
+ {{/each}}
41
+
42
+ {{#each sheet.dashes as |dash|}}
43
+ <line x1="{{math ../sheet.rail.dashLeftX '+' 1}}" y1="{{dash.y}}" x2="{{math ../sheet.rail.dashLeftX '+' (math ../sheet.rail.w '-' 1)}}" y2="{{dash.y}}" stroke="{{ternary dash.dark ../sheet.rail.dark ../sheet.rail.light}}" stroke-width="2" stroke-linecap="round"/>
44
+ <line x1="{{math ../sheet.rail.dashRightX '+' 1}}" y1="{{dash.y}}" x2="{{math ../sheet.rail.dashRightX '+' (math ../sheet.rail.w '-' 1)}}" y2="{{dash.y}}" stroke="{{ternary dash.dark ../sheet.rail.dark ../sheet.rail.light}}" stroke-width="2" stroke-linecap="round"/>
45
+ {{/each}}
46
+ <rect x="{{sheet.rail.ruleLeftX}}" y="{{sheet.rail.ruleTop}}" width="{{sheet.rail.ruleW}}" height="{{sheet.rail.ruleH}}" fill="{{sheet.rail.dark}}"/>
47
+ <rect x="{{sheet.rail.ruleRightX}}" y="{{sheet.rail.ruleTop}}" width="{{sheet.rail.ruleW}}" height="{{sheet.rail.ruleH}}" fill="{{sheet.rail.dark}}"/>
48
+
49
+ {{#each sheet.numbers as |number|}}
50
+ <text x="{{../sheet.numberX}}" y="{{number.y}}" dy=".3em" font-size="12" font-weight="400" fill="#64748B" text-anchor="start">{{number.text}}</text>
51
+ {{/each}}
52
+
53
+ {{#each sheet.levels as |level|}}
54
+ <text x="{{../sheet.levelX}}" y="{{level.y}}" transform="rotate(-90 {{../sheet.levelX}} {{level.y}})" font-size="12" font-weight="400" fill="#94A3B8" text-anchor="middle" dy=".35em">{{level.text}}</text>
55
+ {{/each}}
56
+
57
+ {{#each columns as |col|}}
58
+ <g transform="translate({{math col.cx '-' 1}}, {{../sheet.tick.top}})">
59
+ {{bar 2 ../sheet.tick.h (object tl=1 tr=1 bl=0 br=0) (toRad 0) fill=(ternary col.domain '#4338CA' '#64748B')}}
60
+ </g>
61
+ {{/each}}
62
+
63
+ {{#each sheet.grid.ys as |line|}}
64
+ <rect x="{{../sheet.grid.x}}" y="{{line.y}}" width="{{../sheet.grid.w}}" height="2" rx="1" fill="#94A3B8"{{#if line.faint}} fill-opacity="0.25"{{/if}}/>
65
+ {{/each}}
66
+
67
+ {{#each series as |line|}}
68
+ <polyline points="{{line.points}}" fill="none" stroke="{{line.stroke}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
69
+ {{/each}}
70
+ </g>
@@ -47,8 +47,8 @@
47
47
  <line x1="69.5" y1="102.5" x2="408.5" y2="102.5" stroke="#E2E8F0" stroke-linecap="round" stroke-dasharray="6 6"/>
48
48
  <line x1="69.5" y1="177.5" x2="408.5" y2="177.5" stroke="#E2E8F0" stroke-linecap="round" stroke-dasharray="6 6"/>
49
49
  <line x1="69.5" y1="252.5" x2="408.5" y2="252.5" stroke="#E2E8F0" stroke-linecap="round" stroke-dasharray="6 6"/>
50
- <text x="53" y="27.5" text-anchor="end" dy=".85em" font-size="12" fill="#94A3B8" direction="ltr">٪ 100</text>
51
- <text x="53" y="177.5" text-anchor="end" dy=".35em" font-size="12" fill="#94A3B8" direction="ltr">٪ 50</text>
50
+ <text x="53" y="25.5" text-anchor="end" dy=".85em" font-size="12" fill="#94A3B8" direction="ltr">٪ 100</text>
51
+ <text x="53" y="175.5" text-anchor="end" dy=".35em" font-size="12" fill="#94A3B8" direction="ltr">٪ 50</text>
52
52
 
53
53
  {{! Value bars + inside/outside percentage label }}
54
54
  {{#each factors as |factor|}}
@@ -66,8 +66,8 @@
66
66
 
67
67
  {{! Domain names under each bar }}
68
68
  {{#each factors as |factor|}}
69
- {{#each factor.lines as |line lineIndex|}}
70
- <text x="{{factor.cx}}" y="{{math 352 '+' (math 18 '*' lineIndex)}}" text-anchor="middle" font-size="14" fill="#334155">{{line}}</text>
69
+ {{#each factor.lines as |line|}}
70
+ <text x="{{line.x}}" y="{{line.y}}" text-anchor="middle" font-size="14" fill="#334155">{{line.text}}</text>
71
71
  {{/each}}
72
72
  {{/each}}
73
73
 
@@ -77,10 +77,10 @@
77
77
  </g>
78
78
 
79
79
  {{! ===== "raw score" header row: legend + per-bar raw/max fractions ===== }}
80
- <text x="38" y="9" text-anchor="start" dy=".3em" font-size="12" fill="#94A3B8">نمره خام</text>
80
+ <text x="38" y="7.5" text-anchor="start" dy=".3em" font-size="12" fill="#94A3B8">نمره خام</text>
81
81
  <path d="M45.5 8L52.5 8M49.5 11L52.5 8L49.5 5" stroke="#94A3B8" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
82
82
  {{#each factors as |factor|}}
83
- <text x="{{factor.cx}}" y="9" text-anchor="middle" dy=".3em" direction="ltr"><tspan font-weight="700" font-size="16" fill="#BE185D">{{factor.mark}}</tspan><tspan font-weight="300" font-size="12" fill="#64748B"> / </tspan><tspan font-weight="400" font-size="13" fill="#64748B">{{factor.max}}</tspan></text>
83
+ <text x="{{factor.scoreX}}" y="9.5" text-anchor="middle" dy=".3em" direction="ltr"><tspan font-weight="700" font-size="16" fill="#BE185D">{{factor.mark}}</tspan><tspan font-weight="300" font-size="12" fill="#64748B"> / </tspan><tspan font-weight="400" font-size="13" fill="#64748B">{{factor.max}}</tspan></text>
84
84
  {{/each}}
85
85
 
86
86
  {{! ===================== Total score ring gauge ===================== }}
@@ -88,13 +88,13 @@
88
88
  {{gauge 70 49 (object tl=6 tr=6 bl=6 br=6) (object start=(toRad -90) end=(toRad 180)) false clip-path="url(#gauge-roll)" fill="#F1F5F9"}}
89
89
  {{#if total.mark}}{{gauge 70 49 (object tl=6 tr=6 bl=6 br=6) (object start=(toRad -90) end=total.zeta) false clip-path="url(#gauge-roll)" fill="url(#gaugeGrad)"}}{{/if}}
90
90
 
91
- <text x="0" y="-2" text-anchor="middle" font-size="24" font-weight="600" fill="#334155" direction="ltr">٪ {{total.pct}}</text>
92
- <text x="0" y="26" text-anchor="middle" font-size="15" font-weight="500" fill="#64748B">{{total.mark}} از {{total.max}}</text>
91
+ <text x="0" y="-4" text-anchor="middle" font-size="24" font-weight="600" fill="#334155" direction="ltr">٪ {{total.pct}}</text>
92
+ <text x="0" y="22" text-anchor="middle" font-size="15" font-weight="500" fill="#64748B">{{total.mark}} از {{total.max}}</text>
93
93
 
94
- <text x="-60" y="-6" text-anchor="middle" font-size="14" fill="#64748B" direction="ltr">٪ 100</text>
94
+ <text x="-60" y="-8" text-anchor="middle" font-size="14" fill="#64748B" direction="ltr">٪ 100</text>
95
95
  <text x="-11" y="-56" text-anchor="middle" font-size="14" fill="#64748B">0</text>
96
96
 
97
- <text x="0" y="104" text-anchor="middle" font-size="16" font-weight="500" fill="#475569">نمره کل</text>
97
+ <text x="0" y="102" text-anchor="middle" font-size="16" font-weight="500" fill="#475569">نمره کل</text>
98
98
  </g>
99
99
 
100
100
  </g>
@@ -1,14 +1,5 @@
1
1
  {{#> layout}}
2
- {{> NEO_main
2
+ {{> NEO_sheet
3
3
  padding=spec.profile.padding.[0]
4
- frameX=32
5
- barX=111
6
- rawX=627.5
7
- rawCenter=645
8
- rawMax=192
9
- levelX=707
10
- showLevels=true
11
- tAxis=false
12
- alertX=0
13
4
  }}
14
5
  {{/layout}}
@@ -1,15 +1,14 @@
1
1
  {{#> layout}}
2
- {{> NEO_long_facets
2
+ {{> NEO_main
3
3
  padding=spec.profile.padding.[1]
4
- cardY=43
5
- trackW=300
6
- rawX=671.5
7
- rawCenter=689
8
- levelX=756
9
- domainChipX=68
10
- domainChipW=44
11
- gridTop=23.5
12
- gridBottom=658.5
4
+ frameX=32
5
+ barX=111
6
+ rawX=627.5
7
+ rawCenter=645
8
+ rawMax=192
9
+ levelX=707
10
+ showLevels=true
13
11
  tAxis=false
12
+ alertX=0
14
13
  }}
15
14
  {{/layout}}
@@ -9,7 +9,7 @@
9
9
  domainChipX=68
10
10
  domainChipW=44
11
11
  gridTop=23.5
12
- gridBottom=454.5
12
+ gridBottom=658.5
13
13
  tAxis=false
14
14
  }}
15
15
  {{/layout}}
@@ -0,0 +1,15 @@
1
+ {{#> layout}}
2
+ {{> NEO_long_facets
3
+ padding=spec.profile.padding.[3]
4
+ cardY=43
5
+ trackW=300
6
+ rawX=671.5
7
+ rawCenter=689
8
+ levelX=756
9
+ domainChipX=68
10
+ domainChipW=44
11
+ gridTop=23.5
12
+ gridBottom=454.5
13
+ tAxis=false
14
+ }}
15
+ {{/layout}}
@@ -1,13 +1,5 @@
1
1
  {{#> layout}}
2
- {{> NEO_main
2
+ {{> NEO_sheet
3
3
  padding=spec.profile.padding.[0]
4
- frameX=62
5
- barX=123
6
- rawX=639.5
7
- rawCenter=657
8
- rawMax=192
9
- showLevels=false
10
- tAxis=true
11
- alertX=0
12
4
  }}
13
5
  {{/layout}}
@@ -1,14 +1,13 @@
1
1
  {{#> layout}}
2
- {{> NEO_long_facets
2
+ {{> NEO_main
3
3
  padding=spec.profile.padding.[1]
4
- cardY=49
5
- trackW=380
6
- rawX=759.5
7
- rawCenter=777
8
- domainChipX=70
9
- domainChipW=40
10
- gridTop=29.5
11
- gridBottom=664.5
4
+ frameX=62
5
+ barX=123
6
+ rawX=639.5
7
+ rawCenter=657
8
+ rawMax=192
9
+ showLevels=false
12
10
  tAxis=true
11
+ alertX=0
13
12
  }}
14
13
  {{/layout}}
@@ -8,7 +8,7 @@
8
8
  domainChipX=70
9
9
  domainChipW=40
10
10
  gridTop=29.5
11
- gridBottom=460.5
11
+ gridBottom=664.5
12
12
  tAxis=true
13
13
  }}
14
14
  {{/layout}}
@@ -0,0 +1,14 @@
1
+ {{#> layout}}
2
+ {{> NEO_long_facets
3
+ padding=spec.profile.padding.[3]
4
+ cardY=49
5
+ trackW=380
6
+ rawX=759.5
7
+ rawCenter=777
8
+ domainChipX=70
9
+ domainChipW=40
10
+ gridTop=29.5
11
+ gridBottom=460.5
12
+ tAxis=true
13
+ }}
14
+ {{/layout}}
@@ -1,7 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(*)"
5
- ]
6
- }
7
- }
@@ -1,91 +0,0 @@
1
- const fs = require("fs/promises");
2
- const FormData = require("form-data");
3
- const path = require("path");
4
- const sharp = require("sharp");
5
- const Handlebars = require("handlebars");
6
- const readline = require("readline");
7
- const https = require("https");
8
- require("dotenv").config();
9
-
10
- // New version image hbs directory
11
- const templateDir = path.join(__dirname, "new-version.hbs");
12
-
13
- // Version to be Released
14
- // Remember you should run this script using 'npm ...' command to have npm environment variables
15
- const version = process.env.npm_package_version;
16
-
17
- // Telegram Bot Info (Bot Token Should Be Manually Provided Using dotenv)
18
- const token = process.env.TELEGRAM_BOT_TOKEN;
19
- const chatId = "-651191778";
20
-
21
- const rl = readline.createInterface({
22
- input: process.stdin,
23
- output: process.stdout,
24
- });
25
-
26
- const releaseNotes = [];
27
-
28
- rl.setPrompt("Release Notes:\n");
29
-
30
- rl.prompt();
31
-
32
- rl.on("line", function (ln) {
33
- releaseNotes.push(ln);
34
- });
35
-
36
- // Press Ctrl+C to Just Exit
37
- rl.on("SIGINT", () => process.exit(0));
38
-
39
- // Press Ctrl+D to Emit 'close' Event and Send Release Notes
40
- rl.on("close", sendReleaseNotes);
41
-
42
- // Returns Buffer of New Version Image
43
- async function createReleaseImage() {
44
- return fs
45
- .readFile(templateDir)
46
- .then((templateBuffer) => {
47
- const template = Handlebars.compile(templateBuffer.toString(), "utf-8");
48
- let ctx = {
49
- version,
50
- };
51
- const xml = template(ctx);
52
-
53
- const buf = Buffer.from(xml, "utf8");
54
- return sharp(buf).resize({ width: 350 }).jpeg({ quality: 100 }).toBuffer();
55
- })
56
- .catch((err) => {
57
- throw err;
58
- });
59
- }
60
-
61
- async function sendReleaseNotes() {
62
- // Text to be Sent
63
- const caption = `Version *${version}* Released!\n\n_Release Notes:_\n${releaseNotes.join("\n")}`;
64
- const photoBuf = await createReleaseImage();
65
-
66
- let form = new FormData();
67
-
68
- form.append("chat_id", chatId);
69
- form.append("photo", photoBuf, { filename: "image.jpeg" });
70
- form.append("caption", caption);
71
- form.append("parse_mode", "Markdown");
72
-
73
- // HTTP Request Options (URL & Headers & Method)
74
- const options = {
75
- hostname: "api.telegram.org",
76
- path: `/bot${token}/sendPhoto`,
77
- protocol: "https:",
78
- };
79
-
80
- form.submit(options, (err, res) => {
81
- console.log(`statusCode: ${res.statusCode}`);
82
-
83
- res.on("data", (d) => {
84
- process.stdout.write(d);
85
- });
86
-
87
- if (err) console.log(err);
88
-
89
- res.emit("end");
90
- });
91
- }
@@ -1,11 +0,0 @@
1
- <?xml version="1.0" encoding="utf-8"?>
2
- <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 400" font-family="Dana">
3
- <rect x="0" y="0" width="500" height="400" fill="#EA6B13"/>
4
- <g transform="translate(250, 200)">
5
- <text x="0" y="-75" fill="white" fill-opacity="0.25" font-size="36" font-weight="800" text-anchor="middle">RISLOO EXTRACTOR CLI</text>
6
- <g transform="translate(0, 65)">
7
- <text x="0" y="0" fill="black" fill-opacity="0.25" font-size="88" font-weight="900" text-anchor="middle">v {{version}}</text>
8
- <text x="0" y="0" fill="white" font-size="96" font-weight="900" text-anchor="middle">v {{version}}</text>
9
- </g>
10
- </g>
11
- </svg>