@workingdevshero/olympia-ait-adapter 0.1.5 → 1.1.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.
- package/README.md +139 -35
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/reader/cell-utils.d.ts +10 -0
- package/dist/reader/cell-utils.d.ts.map +1 -0
- package/dist/reader/cell-utils.js +40 -0
- package/dist/reader/cell-utils.js.map +1 -0
- package/dist/reader/parse-day-sheet.d.ts +0 -1
- package/dist/reader/parse-day-sheet.d.ts.map +1 -1
- package/dist/reader/parse-day-sheet.js +95 -75
- package/dist/reader/parse-day-sheet.js.map +1 -1
- package/dist/reader/parse-warmup-workbook.d.ts +13 -0
- package/dist/reader/parse-warmup-workbook.d.ts.map +1 -0
- package/dist/reader/parse-warmup-workbook.js +154 -0
- package/dist/reader/parse-warmup-workbook.js.map +1 -0
- package/dist/reader/parse-workbook.d.ts +2 -2
- package/dist/reader/parse-workbook.d.ts.map +1 -1
- package/dist/reader/parse-workbook.js +1 -6
- package/dist/reader/parse-workbook.js.map +1 -1
- package/dist/schema/program.d.ts +398 -67
- package/dist/schema/program.d.ts.map +1 -1
- package/dist/schema/program.js +135 -28
- package/dist/schema/program.js.map +1 -1
- package/dist/schema/warmup.d.ts +426 -0
- package/dist/schema/warmup.d.ts.map +1 -0
- package/dist/schema/warmup.js +100 -0
- package/dist/schema/warmup.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +51 -18
- package/dist/server.js.map +1 -1
- package/dist/tools/program-json-to-xlsx.d.ts +3 -0
- package/dist/tools/program-json-to-xlsx.d.ts.map +1 -0
- package/dist/tools/program-json-to-xlsx.js +5 -0
- package/dist/tools/program-json-to-xlsx.js.map +1 -0
- package/dist/tools/program-xlsx-to-json.d.ts +3 -0
- package/dist/tools/program-xlsx-to-json.d.ts.map +1 -0
- package/dist/tools/program-xlsx-to-json.js +5 -0
- package/dist/tools/program-xlsx-to-json.js.map +1 -0
- package/dist/tools/warmup-json-to-xlsx.d.ts +3 -0
- package/dist/tools/warmup-json-to-xlsx.d.ts.map +1 -0
- package/dist/tools/warmup-json-to-xlsx.js +5 -0
- package/dist/tools/warmup-json-to-xlsx.js.map +1 -0
- package/dist/tools/warmup-xlsx-to-json.d.ts +3 -0
- package/dist/tools/warmup-xlsx-to-json.d.ts.map +1 -0
- package/dist/tools/warmup-xlsx-to-json.js +5 -0
- package/dist/tools/warmup-xlsx-to-json.js.map +1 -0
- package/dist/writer/build-warmup-workbook.d.ts +3 -0
- package/dist/writer/build-warmup-workbook.d.ts.map +1 -0
- package/dist/writer/build-warmup-workbook.js +101 -0
- package/dist/writer/build-warmup-workbook.js.map +1 -0
- package/dist/writer/build-workbook.d.ts +2 -2
- package/dist/writer/build-workbook.d.ts.map +1 -1
- package/dist/writer/build-workbook.js +19 -23
- package/dist/writer/build-workbook.js.map +1 -1
- package/dist/writer/layout.d.ts +19 -7
- package/dist/writer/layout.d.ts.map +1 -1
- package/dist/writer/layout.js +40 -21
- package/dist/writer/layout.js.map +1 -1
- package/dist/writer/stamp-exercise.d.ts +13 -5
- package/dist/writer/stamp-exercise.d.ts.map +1 -1
- package/dist/writer/stamp-exercise.js +25 -18
- package/dist/writer/stamp-exercise.js.map +1 -1
- package/dist/writer/supplemental-template.d.ts +3 -2
- package/dist/writer/supplemental-template.d.ts.map +1 -1
- package/dist/writer/supplemental-template.js +15 -15
- package/dist/writer/supplemental-template.js.map +1 -1
- package/package.json +5 -4
- package/skills/olympia-authoring/SKILL.md +140 -0
package/README.md
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
# olympia-ait-adapter
|
|
6
6
|
|
|
7
|
-
MCP adapter that converts Olympia Fitness & Performance
|
|
8
|
-
|
|
9
|
-
[Automate It](https://automate.it.com) framework
|
|
10
|
-
strength-and-conditioning
|
|
11
|
-
touching XLSX directly.
|
|
7
|
+
MCP adapter that converts Olympia Fitness & Performance XLSX workbooks —
|
|
8
|
+
training programs and their companion warm-up documents — to and from strict
|
|
9
|
+
JSON schemas. Built for the [Automate It](https://automate.it.com) framework
|
|
10
|
+
so AI agents can author new strength-and-conditioning paperwork from prior
|
|
11
|
+
client data without ever touching XLSX directly.
|
|
12
12
|
|
|
13
13
|
## Why this exists
|
|
14
14
|
|
|
@@ -17,37 +17,48 @@ file, one sheet per training day, with merged cells, custom borders, an
|
|
|
17
17
|
embedded logo, and a domain-specific notation (`12E`, `:20E`, `12EL`,
|
|
18
18
|
`x3 Rest :60`, etc.). Asking an LLM to edit XLSX directly is fragile: it
|
|
19
19
|
corrupts merges, loses the logo, and can't reason about the implicit
|
|
20
|
-
|
|
20
|
+
multi-week / 1A-1B / set-by-set structure encoded in the visual layout.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Each program also ships with a **companion warm-up workbook** — same client,
|
|
23
|
+
same date, paired day-for-day (the "Day 4" warm-up column is performed
|
|
24
|
+
before the "Day 4" program sheet). It's a completely different layout: one
|
|
25
|
+
sheet where each day is a column of titled sections of freeform exercise
|
|
26
|
+
lines.
|
|
27
|
+
|
|
28
|
+
This package gives the agent a clean structured view of both document types
|
|
23
29
|
and guarantees well-formed output: the LLM reads JSON, reasons about it,
|
|
24
30
|
writes JSON, and the adapter handles all the visual fidelity on the way
|
|
25
31
|
back to XLSX.
|
|
26
32
|
|
|
27
33
|
## What it does
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
Four MCP tools, exposed over stdio:
|
|
30
36
|
|
|
31
|
-
| Tool
|
|
32
|
-
|
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
37
|
+
| Tool | Direction | Purpose |
|
|
38
|
+
| ------------------------------ | ----------- | ---------------------------------------------------------------- |
|
|
39
|
+
| `olympia_program_xlsx_to_json` | XLSX → JSON | Parse an Olympia program workbook into a typed `Program`. |
|
|
40
|
+
| `olympia_program_json_to_xlsx` | JSON → XLSX | Render a `Program` back to a styled .xlsx on disk. |
|
|
41
|
+
| `olympia_warmup_xlsx_to_json` | XLSX → JSON | Parse a companion warm-up workbook into a typed `WarmUpDocument`. |
|
|
42
|
+
| `olympia_warmup_json_to_xlsx` | JSON → XLSX | Render a `WarmUpDocument` back to a styled .xlsx on disk. |
|
|
35
43
|
|
|
36
|
-
The JSON
|
|
37
|
-
|
|
44
|
+
The JSON schemas are defined once in `src/schema/program.ts` and
|
|
45
|
+
`src/schema/warmup.ts` using Zod and surface as:
|
|
38
46
|
|
|
39
47
|
1. The TypeScript type used internally
|
|
40
48
|
2. The runtime validator at each tool boundary
|
|
41
49
|
3. The JSON Schema published as each tool's `inputSchema` so the LLM sees
|
|
42
50
|
the full typed contract at discovery time
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
All object schemas are strict: unknown fields are rejected with an error
|
|
53
|
+
rather than silently stripped, so callers authoring against a stale
|
|
54
|
+
contract fail loudly.
|
|
55
|
+
|
|
56
|
+
## JSON shape — programs
|
|
45
57
|
|
|
46
58
|
```ts
|
|
47
59
|
type Program = {
|
|
48
60
|
client: string; // "Bobby Galli"
|
|
49
61
|
programDate: string; // "4.26"
|
|
50
|
-
weekDates?: (string | null)[]; // optional, length 6
|
|
51
62
|
days: Day[];
|
|
52
63
|
};
|
|
53
64
|
|
|
@@ -66,20 +77,21 @@ type Exercise = {
|
|
|
66
77
|
name: string; // "2DB Alternating Bench Press"
|
|
67
78
|
groupLabel?: string; // "1A", "1B", "2A"… matches /^\d+[A-Z]$/
|
|
68
79
|
notes?: string;
|
|
69
|
-
sets: (string | null)[][]; // [setIdx][weekIdx];
|
|
80
|
+
sets: (string | null)[][]; // [setIdx][weekIdx]; one entry per week (4 or 6)
|
|
81
|
+
weights?: (string | null)[][]; // Weight column, same shape as sets
|
|
70
82
|
};
|
|
71
83
|
|
|
72
84
|
type SupplementalNote = {
|
|
73
85
|
row: number; // offset from "Sport Specific/" label row
|
|
74
|
-
col: string; // "
|
|
86
|
+
col: string; // a week's Reps column: "D" | "F" | … (one per week)
|
|
75
87
|
value: string;
|
|
76
88
|
};
|
|
77
89
|
```
|
|
78
90
|
|
|
79
91
|
### Prescription cell grammar
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
Each `sets[setIdx][weekIdx]` is a single opaque string encoding **reps**,
|
|
94
|
+
**time**, or **breaths**:
|
|
83
95
|
|
|
84
96
|
| Format | Meaning |
|
|
85
97
|
| --------------------- | ------------------------------------ |
|
|
@@ -90,10 +102,36 @@ single opaque string encoding **reps**, **time**, or **breaths**:
|
|
|
90
102
|
| free-form strings | circuit-style instructions, OK too |
|
|
91
103
|
| `null` | empty cell |
|
|
92
104
|
|
|
93
|
-
The schema enforces shape (6 weeks per set
|
|
94
|
-
section kinds) but does **not** validate the grammar of each prescription
|
|
105
|
+
The schema enforces shape (1-6 weeks per set and the same count for every
|
|
106
|
+
set in a program, group label regex, allowed section kinds) but does **not** validate the grammar of each prescription
|
|
95
107
|
string — Olympia's own files include free-form entries in some spots.
|
|
96
108
|
|
|
109
|
+
The `Date:` cells in each sheet's header row are **intentionally left
|
|
110
|
+
blank** — clients hand-write the week dates on the printed sheet, so the
|
|
111
|
+
schema has no field for them and the writer never fills them.
|
|
112
|
+
|
|
113
|
+
### Weight column
|
|
114
|
+
|
|
115
|
+
Each week has a Weight cell beside its reps cell. Most are left empty for
|
|
116
|
+
the client to fill in by hand, but coaches type starting loads (`25`) and
|
|
117
|
+
equipment settings there too: a band colour (`Yel`), a box height
|
|
118
|
+
(`12" + 2 Pads`), a rack position (`#10`). `weights` carries those cells
|
|
119
|
+
as free text, in the same shape as `sets`, and is omitted when every cell
|
|
120
|
+
is empty.
|
|
121
|
+
|
|
122
|
+
### Week count and sheet layout
|
|
123
|
+
|
|
124
|
+
Olympia programs run 4 or 6 weeks. The week count is the length of each
|
|
125
|
+
set row; the writer lays the sheet out with exactly that many week pairs
|
|
126
|
+
and puts the Notes column right after the last one (L for 4 weeks, P for
|
|
127
|
+
6). The reader finds the table from its `Exercise | Reps | Weight …`
|
|
128
|
+
header row, so it handles both lengths and the two header positions seen
|
|
129
|
+
in coach-built files (row 4 or row 5).
|
|
130
|
+
|
|
131
|
+
Warm-up exercises listed inside a day sheet are closed by a `Warmup N`
|
|
132
|
+
label row; they decode into a `warmup` section and are written back the
|
|
133
|
+
same way.
|
|
134
|
+
|
|
97
135
|
### Supplemental area
|
|
98
136
|
|
|
99
137
|
The bottom of every day sheet is a trainer-managed "Sport Specific /
|
|
@@ -107,6 +145,46 @@ drills, recovery work). The adapter:
|
|
|
107
145
|
"Supplemental" labels) at the bottom of every day, then layers any
|
|
108
146
|
supplied `supplementalNotes` on top.
|
|
109
147
|
|
|
148
|
+
## JSON shape — warm-ups
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
type WarmUpDocument = {
|
|
152
|
+
client: string; // "Bobby Galli" — same as companion program
|
|
153
|
+
warmUpDate: string; // "4.26" — matches the companion programDate
|
|
154
|
+
days: WarmUpDay[];
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
type WarmUpDay = {
|
|
158
|
+
name: string; // "Day 4" — matches a program day sheet name
|
|
159
|
+
sections: WarmUpSection[];
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
type WarmUpSection = {
|
|
163
|
+
title: string; // "Foam Roll", "Prep \ Activation X2 Rounds"
|
|
164
|
+
items: WarmUpItem[];
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
type WarmUpItem = {
|
|
168
|
+
text: string; // "Pecs :45E" — prescription inline, never parsed
|
|
169
|
+
note?: string; // "(^R.F.E on SB, F-Roller for Balance^)"
|
|
170
|
+
};
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The warm-up layout is days-as-columns on a single sheet (A, C, E, … with
|
|
174
|
+
empty gutter columns between), sections row-aligned across days. Section
|
|
175
|
+
headers are bold; exercise lines are plain with the prescription shorthand
|
|
176
|
+
embedded in the text (`:45E`, `X8br`, `X4ES` — trainer conventions, kept
|
|
177
|
+
opaque); note lines render in italics under their exercise. When decoding,
|
|
178
|
+
italic or fully-parenthesized lines are treated as notes attached to the
|
|
179
|
+
item above — so never author an item whose entire text is parenthesized.
|
|
180
|
+
|
|
181
|
+
## Skill
|
|
182
|
+
|
|
183
|
+
`skills/olympia-authoring/SKILL.md` (shipped in the npm package) is an
|
|
184
|
+
agent-facing technical reference covering both formats: the schemas, the
|
|
185
|
+
prescription notation, the program/warm-up pairing rules, and the
|
|
186
|
+
decode → author → encode workflow.
|
|
187
|
+
|
|
110
188
|
## Installation
|
|
111
189
|
|
|
112
190
|
```bash
|
|
@@ -143,29 +221,38 @@ Claude or another MCP client, add an entry like:
|
|
|
143
221
|
}
|
|
144
222
|
```
|
|
145
223
|
|
|
146
|
-
|
|
224
|
+
All tools take absolute paths to .xlsx files on the local filesystem.
|
|
147
225
|
|
|
148
226
|
## Repo layout
|
|
149
227
|
|
|
150
228
|
```
|
|
151
229
|
src/
|
|
152
|
-
schema/
|
|
230
|
+
schema/
|
|
231
|
+
program.ts # Zod schema for programs — single source of truth
|
|
232
|
+
warmup.ts # Zod schema for warm-up documents
|
|
153
233
|
reader/
|
|
234
|
+
cell-utils.ts # Shared ExcelJS cell-value helpers
|
|
154
235
|
parse-workbook.ts # Workbook → Program JSON
|
|
155
236
|
parse-day-sheet.ts # One sheet, section detection, supplemental capture
|
|
237
|
+
parse-warmup-workbook.ts # Warm-up workbook → WarmUpDocument JSON
|
|
156
238
|
writer/
|
|
157
239
|
build-workbook.ts # Program → ExcelJS workbook
|
|
240
|
+
build-warmup-workbook.ts # WarmUpDocument → ExcelJS workbook
|
|
158
241
|
layout.ts # Column widths, row positions, logo anchor
|
|
159
242
|
styles.ts # All borders/fonts/fills as constants
|
|
160
243
|
stamp-exercise.ts # One exercise block (variable sets, group borders)
|
|
161
244
|
supplemental-template.ts # Fixed bottom template + supplementalNotes overlay
|
|
162
245
|
tools/
|
|
163
|
-
xlsx-to-json.ts
|
|
164
|
-
json-to-xlsx.ts
|
|
165
|
-
|
|
246
|
+
program-xlsx-to-json.ts # MCP handler
|
|
247
|
+
program-json-to-xlsx.ts # MCP handler
|
|
248
|
+
warmup-xlsx-to-json.ts # MCP handler
|
|
249
|
+
warmup-json-to-xlsx.ts # MCP handler
|
|
250
|
+
server.ts # Bootstrap, registers all four tools
|
|
166
251
|
assets/
|
|
167
252
|
olympia-logo.png # Extracted from sample workbooks; embedded in output
|
|
168
253
|
|
|
254
|
+
skills/
|
|
255
|
+
olympia-authoring/SKILL.md # Agent-facing technical reference for both formats
|
|
169
256
|
samples/ # Real Olympia .xlsx files used as round-trip fixtures
|
|
170
257
|
scripts/ # Inspection helpers used during development
|
|
171
258
|
tests/ # vitest — schema, reader, round-trip, MCP smoke
|
|
@@ -181,16 +268,33 @@ npm run typecheck # tsc --noEmit
|
|
|
181
268
|
|
|
182
269
|
Coverage:
|
|
183
270
|
|
|
184
|
-
- **`schema.test.ts`** — Zod rejects malformed input (week count,
|
|
185
|
-
label regex, section kinds, missing fields).
|
|
186
|
-
- **`reader.test.ts`** — All three
|
|
187
|
-
|
|
188
|
-
|
|
271
|
+
- **`schema.test.ts`** — Zod rejects malformed program input (week count,
|
|
272
|
+
group label regex, section kinds, missing fields).
|
|
273
|
+
- **`reader.test.ts`** — All three program samples parse into schema-valid
|
|
274
|
+
`Program` documents; spot checks on Bobby's first exercise, Giulianna's
|
|
275
|
+
variable set counts, exercise notes, supplemental notes.
|
|
276
|
+
- **`week-count.test.ts`** — A 4-week program is laid out with four week
|
|
277
|
+
pairs and round-trips; a hand-built 4-week sheet (header on row 4,
|
|
278
|
+
`Warmup N` labels, Notes in column L) decodes correctly.
|
|
279
|
+
- **`weights.test.ts`** — Weight-column cells are written beside their
|
|
280
|
+
reps, round-trip, and must match the shape of `sets`.
|
|
189
281
|
- **`round-trip.test.ts`** — The primary determinism guarantee: for each
|
|
190
|
-
sample, `parse → write → parse` produces JSON equal to the
|
|
282
|
+
program sample, `parse → write → parse` produces JSON equal to the
|
|
283
|
+
original.
|
|
284
|
+
- **`warmup-doc-schema.test.ts`** — Zod accept/reject tables for the
|
|
285
|
+
warm-up document schema.
|
|
286
|
+
- **`warmup-doc-reader.test.ts`** — The warm-up sample parses into a
|
|
287
|
+
schema-valid `WarmUpDocument` with verbatim spot checks (including the
|
|
288
|
+
note-attachment rule); warning paths for irregular layouts.
|
|
289
|
+
- **`warmup-doc-writer.test.ts`** — Day/section/item/note styling and
|
|
290
|
+
cross-day section alignment, verified by re-reading the written file.
|
|
291
|
+
- **`warmup-doc-round-trip.test.ts`** — Warm-up determinism: the sample
|
|
292
|
+
and an authored document both survive `parse → write → parse` /
|
|
293
|
+
`write → parse` unchanged.
|
|
191
294
|
- **`mcp-smoke.test.ts`** — End-to-end MCP server check via the in-memory
|
|
192
|
-
transport: tool discovery, round-
|
|
193
|
-
|
|
295
|
+
transport: tool discovery (all four tools), round-trips via
|
|
296
|
+
`client.callTool` for both document types, schema rejection of malformed
|
|
297
|
+
input at the SDK boundary.
|
|
194
298
|
|
|
195
299
|
## Determinism
|
|
196
300
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
export { Program, Day, Section, SectionKind, Exercise, SetRow, Cell, SupplementalNote,
|
|
1
|
+
export { Program, Day, Section, SectionKind, Exercise, SetRow, Cell, WeightCell, SupplementalNote, ProgramXlsxToJsonInput, ProgramXlsxToJsonOutput, ProgramJsonToXlsxInput, ProgramJsonToXlsxOutput, } from "./schema/program.js";
|
|
2
|
+
export { WarmUpDocument, WarmUpDay, WarmUpSection, WarmUpItem, WarmUpXlsxToJsonInput, WarmUpXlsxToJsonOutput, WarmUpJsonToXlsxInput, WarmUpJsonToXlsxOutput, } from "./schema/warmup.js";
|
|
2
3
|
export { createServer } from "./server.js";
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
4
|
+
export { parseProgramWorkbook } from "./reader/parse-workbook.js";
|
|
5
|
+
export { buildProgramWorkbook } from "./writer/build-workbook.js";
|
|
6
|
+
export { parseWarmUpWorkbook } from "./reader/parse-warmup-workbook.js";
|
|
7
|
+
export { buildWarmUpWorkbook } from "./writer/build-warmup-workbook.js";
|
|
5
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,OAAO,EACP,GAAG,EACH,OAAO,EACP,WAAW,EACX,QAAQ,EACR,MAAM,EACN,IAAI,EACJ,gBAAgB,EAChB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,OAAO,EACP,GAAG,EACH,OAAO,EACP,WAAW,EACX,QAAQ,EACR,MAAM,EACN,IAAI,EACJ,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,cAAc,EACd,SAAS,EACT,aAAa,EACb,UAAU,EACV,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// Programmatic entry point. The package's primary use is the MCP CLI
|
|
2
2
|
// (registered under the `bin` field), but the schema and server factory
|
|
3
3
|
// are also exported for callers that want to embed or compose the adapter.
|
|
4
|
-
export { Program, Day, Section, SectionKind, Exercise, SetRow, Cell, SupplementalNote,
|
|
4
|
+
export { Program, Day, Section, SectionKind, Exercise, SetRow, Cell, WeightCell, SupplementalNote, ProgramXlsxToJsonInput, ProgramXlsxToJsonOutput, ProgramJsonToXlsxInput, ProgramJsonToXlsxOutput, } from "./schema/program.js";
|
|
5
|
+
export { WarmUpDocument, WarmUpDay, WarmUpSection, WarmUpItem, WarmUpXlsxToJsonInput, WarmUpXlsxToJsonOutput, WarmUpJsonToXlsxInput, WarmUpJsonToXlsxOutput, } from "./schema/warmup.js";
|
|
5
6
|
export { createServer } from "./server.js";
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
7
|
+
export { parseProgramWorkbook } from "./reader/parse-workbook.js";
|
|
8
|
+
export { buildProgramWorkbook } from "./writer/build-workbook.js";
|
|
9
|
+
export { parseWarmUpWorkbook } from "./reader/parse-warmup-workbook.js";
|
|
10
|
+
export { buildWarmUpWorkbook } from "./writer/build-warmup-workbook.js";
|
|
8
11
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,wEAAwE;AACxE,2EAA2E;AAE3E,OAAO,EACL,OAAO,EACP,GAAG,EACH,OAAO,EACP,WAAW,EACX,QAAQ,EACR,MAAM,EACN,IAAI,EACJ,gBAAgB,EAChB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,wEAAwE;AACxE,2EAA2E;AAE3E,OAAO,EACL,OAAO,EACP,GAAG,EACH,OAAO,EACP,WAAW,EACX,QAAQ,EACR,MAAM,EACN,IAAI,EACJ,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,cAAc,EACd,SAAS,EACT,aAAa,EACb,UAAU,EACV,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type ExcelJS from "exceljs";
|
|
2
|
+
/**
|
|
3
|
+
* Best-effort plain-text extraction from an ExcelJS cell value: handles
|
|
4
|
+
* strings, numbers, booleans, dates, rich text, hyperlink-style `{ text }`
|
|
5
|
+
* objects, and formula results. Anything else becomes "".
|
|
6
|
+
*/
|
|
7
|
+
export declare function cellText(value: ExcelJS.CellValue | undefined | null): string;
|
|
8
|
+
export declare function cellOrNull(value: ExcelJS.CellValue | undefined | null): string | null;
|
|
9
|
+
export declare function isMergeContinuation(cell: ExcelJS.Cell): boolean;
|
|
10
|
+
//# sourceMappingURL=cell-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell-utils.d.ts","sourceRoot":"","sources":["../../src/reader/cell-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAEnC;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAmB5E;AAED,wBAAgB,UAAU,CACxB,KAAK,EAAE,OAAO,CAAC,SAAS,GAAG,SAAS,GAAG,IAAI,GAC1C,MAAM,GAAG,IAAI,CAGf;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,GAAG,OAAO,CAI/D"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort plain-text extraction from an ExcelJS cell value: handles
|
|
3
|
+
* strings, numbers, booleans, dates, rich text, hyperlink-style `{ text }`
|
|
4
|
+
* objects, and formula results. Anything else becomes "".
|
|
5
|
+
*/
|
|
6
|
+
export function cellText(value) {
|
|
7
|
+
if (value === null || value === undefined)
|
|
8
|
+
return "";
|
|
9
|
+
if (typeof value === "string")
|
|
10
|
+
return value;
|
|
11
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
12
|
+
return String(value);
|
|
13
|
+
if (value instanceof Date)
|
|
14
|
+
return value.toISOString();
|
|
15
|
+
if (typeof value === "object") {
|
|
16
|
+
if ("richText" in value && Array.isArray(value.richText)) {
|
|
17
|
+
return value.richText
|
|
18
|
+
.map((r) => r.text)
|
|
19
|
+
.join("");
|
|
20
|
+
}
|
|
21
|
+
if ("text" in value && typeof value.text === "string") {
|
|
22
|
+
return value.text;
|
|
23
|
+
}
|
|
24
|
+
if ("result" in value) {
|
|
25
|
+
return cellText(value.result);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
export function cellOrNull(value) {
|
|
31
|
+
const t = cellText(value).trim();
|
|
32
|
+
return t === "" ? null : t;
|
|
33
|
+
}
|
|
34
|
+
export function isMergeContinuation(cell) {
|
|
35
|
+
if (!cell.isMerged)
|
|
36
|
+
return false;
|
|
37
|
+
const master = cell.master;
|
|
38
|
+
return master !== undefined && master.address !== cell.address;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=cell-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell-utils.js","sourceRoot":"","sources":["../../src/reader/cell-utils.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,KAA2C;IAClE,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACrD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IAClF,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;IACtD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,UAAU,IAAI,KAAK,IAAI,KAAK,CAAC,OAAO,CAAE,KAA+B,CAAC,QAAQ,CAAC,EAAE,CAAC;YACpF,OAAQ,KAA0C,CAAC,QAAQ;iBACxD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;iBAClB,IAAI,CAAC,EAAE,CAAC,CAAC;QACd,CAAC;QACD,IAAI,MAAM,IAAI,KAAK,IAAI,OAAQ,KAA2B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7E,OAAQ,KAA0B,CAAC,IAAI,CAAC;QAC1C,CAAC;QACD,IAAI,QAAQ,IAAI,KAAK,EAAE,CAAC;YACtB,OAAO,QAAQ,CAAE,KAAuC,CAAC,MAAM,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,KAA2C;IAE3C,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IACjC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAkB;IACpD,IAAI,CAAC,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACjC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,OAAO,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,CAAC;AACjE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-day-sheet.d.ts","sourceRoot":"","sources":["../../src/reader/parse-day-sheet.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EACV,gBAAgB,EAEhB,GAAG,EAIJ,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"parse-day-sheet.d.ts","sourceRoot":"","sources":["../../src/reader/parse-day-sheet.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EACV,gBAAgB,EAEhB,GAAG,EAIJ,MAAM,sBAAsB,CAAC;AAG9B,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IAC1B,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACvC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAyFD,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,CAAC,SAAS,GAAG,SAAS,CA6H9D"}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
["conditioning", "conditioning"],
|
|
9
|
-
]);
|
|
1
|
+
import { cellText, cellOrNull, isMergeContinuation } from "./cell-utils.js";
|
|
2
|
+
const MAX_WEEK_COUNT = 6;
|
|
3
|
+
const FIRST_WEEK_COL_INDEX = 4; // column D
|
|
4
|
+
const HEADER_SEARCH_ROWS = 12;
|
|
5
|
+
// Layout of sheets written by this package, used when a sheet has no
|
|
6
|
+
// recognizable "Exercise | Reps | Weight …" header row.
|
|
7
|
+
const DEFAULT_HEADER_ROW = 5;
|
|
10
8
|
const SUPPLEMENTAL_TRIGGER_LABELS = new Set([
|
|
11
9
|
"sport specific",
|
|
12
10
|
"sport specific/",
|
|
@@ -14,70 +12,62 @@ const SUPPLEMENTAL_TRIGGER_LABELS = new Set([
|
|
|
14
12
|
"conditioning",
|
|
15
13
|
"circuit",
|
|
16
14
|
]);
|
|
17
|
-
// Columns scanned for supplementalNotes. Excludes A/B (cosmetic narrow margins)
|
|
18
|
-
// and Q (rarely used).
|
|
19
|
-
const SUPPLEMENTAL_COLS = ["C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P"];
|
|
20
15
|
function isGroupLabel(s) {
|
|
21
16
|
return /^\d+[A-Z]$/.test(s);
|
|
22
17
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if ("
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
18
|
+
/** "Warmup 1", "Warm Up 2", "Warm-up 3": the label row closing an in-sheet warm-up exercise. */
|
|
19
|
+
function isWarmupLabel(s) {
|
|
20
|
+
return /^warm[\s-]?up\s*#?\d+$/i.test(s);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Locate the exercise table from its header row ("Exercise | Reps | Weight |
|
|
24
|
+
* Reps | Weight | …"). Olympia's sheets vary: the header sits on row 4 or 5
|
|
25
|
+
* and a program runs 4 or 6 weeks, with the Notes column immediately right
|
|
26
|
+
* of the last week pair. The body starts two rows below the header.
|
|
27
|
+
*/
|
|
28
|
+
function detectLayout(ws) {
|
|
29
|
+
let headerRow = DEFAULT_HEADER_ROW;
|
|
30
|
+
let weekCount = MAX_WEEK_COUNT;
|
|
31
|
+
for (let r = 1; r <= HEADER_SEARCH_ROWS; r++) {
|
|
32
|
+
const row = ws.getRow(r);
|
|
33
|
+
if (cellText(row.getCell("C").value).trim().toLowerCase() !== "exercise")
|
|
34
|
+
continue;
|
|
35
|
+
let found = 0;
|
|
36
|
+
while (found < MAX_WEEK_COUNT &&
|
|
37
|
+
cellText(row.getCell(FIRST_WEEK_COL_INDEX + 2 * found).value).trim().toLowerCase() === "reps") {
|
|
38
|
+
found++;
|
|
43
39
|
}
|
|
40
|
+
// An "Exercise" cell without Reps headers beside it (a title, a note)
|
|
41
|
+
// is not the header row; keep looking.
|
|
42
|
+
if (found === 0)
|
|
43
|
+
continue;
|
|
44
|
+
headerRow = r;
|
|
45
|
+
weekCount = found;
|
|
46
|
+
break;
|
|
44
47
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
function isMergeContinuation(cell) {
|
|
52
|
-
if (!cell.isMerged)
|
|
53
|
-
return false;
|
|
54
|
-
const master = cell.master;
|
|
55
|
-
return master !== undefined && master.address !== cell.address;
|
|
48
|
+
const weekCols = Array.from({ length: weekCount }, (_, i) => FIRST_WEEK_COL_INDEX + 2 * i);
|
|
49
|
+
return {
|
|
50
|
+
firstBodyRow: headerRow + 2,
|
|
51
|
+
weekCols,
|
|
52
|
+
notesCol: FIRST_WEEK_COL_INDEX + 2 * weekCount,
|
|
53
|
+
};
|
|
56
54
|
}
|
|
57
|
-
function readWeekRow(row) {
|
|
58
|
-
return
|
|
55
|
+
function readWeekRow(row, layout) {
|
|
56
|
+
return layout.weekCols.map((col) => cellOrNull(row.getCell(col).value));
|
|
59
57
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (t === "" || t.toLowerCase() === "date:")
|
|
64
|
-
return null;
|
|
65
|
-
return t;
|
|
66
|
-
});
|
|
58
|
+
/** The Weight cell sits immediately right of each week's Reps cell. */
|
|
59
|
+
function readWeightRow(row, layout) {
|
|
60
|
+
return layout.weekCols.map((col) => cellOrNull(row.getCell(col + 1).value));
|
|
67
61
|
}
|
|
68
62
|
export function parseDaySheet(ws) {
|
|
69
63
|
const warnings = [];
|
|
70
64
|
const sections = [];
|
|
65
|
+
const layout = detectLayout(ws);
|
|
71
66
|
let current = { kind: "main", exercises: [] };
|
|
72
67
|
let exercise = null;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const dates = readWeekDates(dateRow);
|
|
77
|
-
if (dates.some((d) => d !== null))
|
|
78
|
-
weekDates = dates;
|
|
79
|
-
}
|
|
80
|
-
const flushExercise = (groupLabel) => {
|
|
68
|
+
// An exercise closed by a "Warmup N" label belongs to the warmup section;
|
|
69
|
+
// every other exercise belongs to main. A change of kind starts a new section.
|
|
70
|
+
const flushExercise = (groupLabel, kind = "main") => {
|
|
81
71
|
if (!exercise)
|
|
82
72
|
return;
|
|
83
73
|
const ex = {
|
|
@@ -88,9 +78,19 @@ export function parseDaySheet(ws) {
|
|
|
88
78
|
ex.groupLabel = groupLabel;
|
|
89
79
|
if (exercise.notes)
|
|
90
80
|
ex.notes = exercise.notes;
|
|
81
|
+
if (exercise.weights.some((row) => row.some((cell) => cell !== null))) {
|
|
82
|
+
ex.weights = exercise.weights;
|
|
83
|
+
}
|
|
84
|
+
if (current.kind !== kind) {
|
|
85
|
+
if (current.exercises.length > 0) {
|
|
86
|
+
sections.push({ kind: current.kind, exercises: current.exercises });
|
|
87
|
+
}
|
|
88
|
+
current = { kind, exercises: [] };
|
|
89
|
+
}
|
|
91
90
|
current.exercises.push(ex);
|
|
92
91
|
exercise = null;
|
|
93
92
|
};
|
|
93
|
+
const noteOf = (row) => cellText(row.getCell(layout.notesCol).value).trim();
|
|
94
94
|
const flushSection = () => {
|
|
95
95
|
flushExercise();
|
|
96
96
|
sections.push({ kind: current.kind, exercises: current.exercises });
|
|
@@ -98,7 +98,7 @@ export function parseDaySheet(ws) {
|
|
|
98
98
|
const lastRow = ws.actualRowCount > 0 ? ws.rowCount : 0;
|
|
99
99
|
let supplementalStartRow;
|
|
100
100
|
let supplementalNotes;
|
|
101
|
-
for (let r =
|
|
101
|
+
for (let r = layout.firstBodyRow; r <= lastRow; r++) {
|
|
102
102
|
const row = ws.getRow(r);
|
|
103
103
|
const cCell = row.getCell("C");
|
|
104
104
|
const cText = cellText(cCell.value).trim();
|
|
@@ -107,26 +107,41 @@ export function parseDaySheet(ws) {
|
|
|
107
107
|
if (SUPPLEMENTAL_TRIGGER_LABELS.has(cLower)) {
|
|
108
108
|
flushSection();
|
|
109
109
|
supplementalStartRow = r;
|
|
110
|
-
supplementalNotes = captureSupplementalNotes(ws, r, lastRow);
|
|
110
|
+
supplementalNotes = captureSupplementalNotes(ws, r, lastRow, layout);
|
|
111
111
|
break;
|
|
112
112
|
}
|
|
113
|
-
//
|
|
114
|
-
|
|
113
|
+
// A group label ("1A") or warm-up label ("Warmup 1") terminates an exercise block.
|
|
114
|
+
const warmupLabel = isWarmupLabel(cText);
|
|
115
|
+
if (cText !== "" && (isGroupLabel(cText) || warmupLabel) && !isMergeContinuation(cCell)) {
|
|
115
116
|
if (!exercise) {
|
|
116
|
-
warnings.push(`row ${r}:
|
|
117
|
+
warnings.push(`row ${r}: label "${cText}" without a preceding exercise name`);
|
|
117
118
|
continue;
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
// A label row with nothing in it, under rows that do hold a
|
|
121
|
+
// prescription, is only there to carry the label of a one-set
|
|
122
|
+
// exercise — it is not a set.
|
|
123
|
+
const labelSets = readWeekRow(row, layout);
|
|
124
|
+
const labelWeights = readWeightRow(row, layout);
|
|
125
|
+
const labelRowEmpty = [...labelSets, ...labelWeights].every((cell) => cell === null);
|
|
126
|
+
const blockHasContent = [...exercise.sets, ...exercise.weights].some((cells) => cells.some((cell) => cell !== null));
|
|
127
|
+
if (!(labelRowEmpty && blockHasContent)) {
|
|
128
|
+
exercise.sets.push(labelSets);
|
|
129
|
+
exercise.weights.push(labelWeights);
|
|
130
|
+
}
|
|
131
|
+
const pNote = noteOf(row);
|
|
121
132
|
if (pNote && !exercise.notes)
|
|
122
133
|
exercise.notes = pNote;
|
|
123
|
-
|
|
134
|
+
if (warmupLabel)
|
|
135
|
+
flushExercise(undefined, "warmup");
|
|
136
|
+
else
|
|
137
|
+
flushExercise(cText);
|
|
124
138
|
continue;
|
|
125
139
|
}
|
|
126
140
|
// Continuation of a merged exercise name?
|
|
127
141
|
if (isMergeContinuation(cCell) && exercise) {
|
|
128
|
-
exercise.sets.push(readWeekRow(row));
|
|
129
|
-
|
|
142
|
+
exercise.sets.push(readWeekRow(row, layout));
|
|
143
|
+
exercise.weights.push(readWeightRow(row, layout));
|
|
144
|
+
const pNote = noteOf(row);
|
|
130
145
|
if (pNote && !exercise.notes)
|
|
131
146
|
exercise.notes = pNote;
|
|
132
147
|
continue;
|
|
@@ -136,9 +151,10 @@ export function parseDaySheet(ws) {
|
|
|
136
151
|
flushExercise();
|
|
137
152
|
exercise = {
|
|
138
153
|
name: cText,
|
|
139
|
-
sets: [readWeekRow(row)],
|
|
154
|
+
sets: [readWeekRow(row, layout)],
|
|
155
|
+
weights: [readWeightRow(row, layout)],
|
|
140
156
|
};
|
|
141
|
-
const pNote =
|
|
157
|
+
const pNote = noteOf(row);
|
|
142
158
|
if (pNote)
|
|
143
159
|
exercise.notes = pNote;
|
|
144
160
|
continue;
|
|
@@ -153,7 +169,6 @@ export function parseDaySheet(ws) {
|
|
|
153
169
|
}
|
|
154
170
|
return {
|
|
155
171
|
sections: nonEmpty,
|
|
156
|
-
...(weekDates ? { weekDates } : {}),
|
|
157
172
|
...(supplementalNotes && supplementalNotes.length > 0
|
|
158
173
|
? { supplementalNotes }
|
|
159
174
|
: {}),
|
|
@@ -171,11 +186,16 @@ export function parseDaySheet(ws) {
|
|
|
171
186
|
* multiple cells appears once). The trigger labels themselves are skipped
|
|
172
187
|
* since they're added back by the writer's fixed template.
|
|
173
188
|
*/
|
|
174
|
-
function captureSupplementalNotes(ws, supplementalStartRow, lastRow) {
|
|
189
|
+
function captureSupplementalNotes(ws, supplementalStartRow, lastRow, layout) {
|
|
175
190
|
const out = [];
|
|
191
|
+
// Scan from column C through the Notes column. Excludes A/B (cosmetic
|
|
192
|
+
// narrow margins) and anything right of Notes.
|
|
193
|
+
const cols = [];
|
|
194
|
+
for (let i = 3; i <= layout.notesCol; i++)
|
|
195
|
+
cols.push(String.fromCharCode(64 + i));
|
|
176
196
|
for (let r = supplementalStartRow; r <= lastRow; r++) {
|
|
177
197
|
const row = ws.getRow(r);
|
|
178
|
-
for (const col of
|
|
198
|
+
for (const col of cols) {
|
|
179
199
|
const cell = row.getCell(col);
|
|
180
200
|
if (isMergeContinuation(cell))
|
|
181
201
|
continue;
|