@dnax/ui 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/components/QAccordion.vue +68 -0
- package/components/QAccordionContent.vue +62 -0
- package/components/QAccordionItem.vue +24 -0
- package/components/QAccordionTrigger.vue +51 -0
- package/components/QActionSheet.vue +159 -0
- package/components/QApp.vue +8 -0
- package/components/QAutocomplete.vue +316 -0
- package/components/QAvatar.vue +92 -0
- package/components/QBackHeader.vue +69 -0
- package/components/QBackTop.vue +125 -0
- package/components/QBadge.vue +55 -0
- package/components/QBottomSheet.vue +177 -0
- package/components/QBottomSheetFooter.vue +9 -0
- package/components/QBottomSheetHeader.vue +35 -0
- package/components/QBottomSheetProvider.vue +62 -0
- package/components/QBottomSheetTrigger.vue +18 -0
- package/components/QBtn.vue +144 -0
- package/components/QBtnGroup.vue +31 -0
- package/components/QBubble.vue +36 -0
- package/components/QBubbleContent.vue +28 -0
- package/components/QBubbleGroup.vue +25 -0
- package/components/QBubbleReactions.vue +40 -0
- package/components/QCard.vue +63 -0
- package/components/QCardActions.vue +27 -0
- package/components/QCardContent.vue +29 -0
- package/components/QCarousel.vue +115 -0
- package/components/QCarouselContent.vue +23 -0
- package/components/QCarouselItem.vue +20 -0
- package/components/QCarouselNav.vue +51 -0
- package/components/QCheckbox.vue +146 -0
- package/components/QChip.vue +91 -0
- package/components/QCircularProgress.vue +122 -0
- package/components/QCol.vue +52 -0
- package/components/QCollapse.vue +141 -0
- package/components/QConfigProvider.vue +164 -0
- package/components/QContainer.vue +201 -0
- package/components/QCountDown.vue +164 -0
- package/components/QCountryPicker.vue +593 -0
- package/components/QDataGrid.vue +491 -0
- package/components/QDatePicker.vue +250 -0
- package/components/QDialog.vue +163 -0
- package/components/QDialogFooter.vue +9 -0
- package/components/QDialogHeader.vue +41 -0
- package/components/QDialogProvider.vue +65 -0
- package/components/QDialogTrigger.vue +18 -0
- package/components/QFab.vue +80 -0
- package/components/QFabAction.vue +48 -0
- package/components/QFilePicker.vue +294 -0
- package/components/QFooter.vue +72 -0
- package/components/QGallery.vue +280 -0
- package/components/QGrid.vue +54 -0
- package/components/QHeader.vue +38 -0
- package/components/QIcon.vue +53 -0
- package/components/QImagePicker.vue +248 -0
- package/components/QImagePreview.vue +225 -0
- package/components/QImagePreviewProvider.vue +35 -0
- package/components/QImg.vue +142 -0
- package/components/QInfiniteScroll.vue +109 -0
- package/components/QInnerLoading.vue +67 -0
- package/components/QInput.vue +289 -0
- package/components/QInputOtp.vue +238 -0
- package/components/QInputPassword.vue +67 -0
- package/components/QInputTag.vue +281 -0
- package/components/QIntersection.vue +119 -0
- package/components/QItem.vue +60 -0
- package/components/QItemSection.vue +47 -0
- package/components/QLinearProgress.vue +94 -0
- package/components/QList.vue +39 -0
- package/components/QLoading.vue +77 -0
- package/components/QLoadingProvider.vue +41 -0
- package/components/QMarquee.vue +56 -0
- package/components/QMessageScroller.vue +10 -0
- package/components/QMessageScrollerButton.vue +48 -0
- package/components/QMessageScrollerContent.vue +31 -0
- package/components/QMessageScrollerItem.vue +49 -0
- package/components/QMessageScrollerProvider.vue +198 -0
- package/components/QMessageScrollerViewport.vue +109 -0
- package/components/QNavMenu.vue +75 -0
- package/components/QNavMenuContent.vue +10 -0
- package/components/QNavMenuItem.vue +60 -0
- package/components/QNavMenuTrigger.vue +96 -0
- package/components/QNotifyProvider.vue +33 -0
- package/components/QNotifyToast.vue +60 -0
- package/components/QPage.vue +56 -0
- package/components/QPagination.vue +195 -0
- package/components/QParallax.vue +124 -0
- package/components/QPullToRefresh.vue +138 -0
- package/components/QRadio.vue +99 -0
- package/components/QRating.vue +105 -0
- package/components/QReorder.vue +191 -0
- package/components/QRollingText.vue +225 -0
- package/components/QRow.vue +23 -0
- package/components/QSafeArea.vue +39 -0
- package/components/QScrollArea.vue +118 -0
- package/components/QSelect.vue +698 -0
- package/components/QSeparator.vue +39 -0
- package/components/QSidebar.vue +153 -0
- package/components/QSidebarContent.vue +14 -0
- package/components/QSidebarFooter.vue +14 -0
- package/components/QSidebarHeader.vue +14 -0
- package/components/QSidebarMenu.vue +9 -0
- package/components/QSidebarMenuButton.vue +59 -0
- package/components/QSidebarMenuItem.vue +9 -0
- package/components/QSidebarTrigger.vue +32 -0
- package/components/QSkeleton.vue +95 -0
- package/components/QSlider.vue +205 -0
- package/components/QSpace.vue +10 -0
- package/components/QSpinner.vue +172 -0
- package/components/QSplitter.vue +145 -0
- package/components/QSticky.vue +181 -0
- package/components/QSwipeCell.vue +217 -0
- package/components/QSwiper.vue +102 -0
- package/components/QSwiperSlide.vue +19 -0
- package/components/QSyntax.vue +171 -0
- package/components/QTab.vue +175 -0
- package/components/QTabPanel.vue +50 -0
- package/components/QTabPanels.vue +106 -0
- package/components/QTable.vue +575 -0
- package/components/QTabs.vue +188 -0
- package/components/QText.vue +316 -0
- package/components/QTextCaption.vue +122 -0
- package/components/QToolbar.vue +28 -0
- package/components/QTooltip.vue +313 -0
- package/components/QUploader.vue +473 -0
- package/components/QVideo.vue +289 -0
- package/components/QVirtualScroll.vue +190 -0
- package/components/internal/QDateCalendar.vue +153 -0
- package/index.ts +151 -0
- package/lib/breakpoints.ts +44 -0
- package/lib/colors.ts +24 -0
- package/lib/config.ts +34 -0
- package/lib/countries.ts +261 -0
- package/lib/formula.ts +630 -0
- package/lib/icons.ts +38 -0
- package/lib/loading.ts +75 -0
- package/lib/overlayBack.ts +124 -0
- package/lib/platform.ts +146 -0
- package/lib/primitive.ts +25 -0
- package/lib/q.ts +287 -0
- package/lib/screen.ts +105 -0
- package/lib/search.ts +35 -0
- package/lib/themeVars.ts +22 -0
- package/lib/useComponentProps.ts +54 -0
- package/lib/utils.ts +7 -0
- package/module.ts +89 -0
- package/package.json +41 -0
- package/scripts/generate-exports.ts +56 -0
- package/styles/main.css +7627 -0
- package/tsconfig.json +33 -0
package/lib/formula.ts
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
// Moteur de formules style tableur pour QDataGrid.
|
|
2
|
+
// Syntaxe : une valeur de cellule qui commence par "=" est une formule :
|
|
3
|
+
// =price * quantity → références par nom de colonne (ligne courante)
|
|
4
|
+
// =SUM(E1:E3) → plages A1, fonctions intégrées
|
|
5
|
+
// =E4 * $B$5 → références A1 (absolues avec $)
|
|
6
|
+
// =IF(A1 > 0, "ok", "ko") → fonctions, comparaisons
|
|
7
|
+
// Opérateurs : + - * / ^ % & = <> < > <= >= · parenthèses · unaires +/- · chaînes "..."/'...'
|
|
8
|
+
// Erreurs : #DIV/0! #NAME? #VALUE! #REF! #N/A #CYCLE! #ERROR! (rendues en rouge par le grid).
|
|
9
|
+
|
|
10
|
+
export class FormulaError {
|
|
11
|
+
readonly code: string
|
|
12
|
+
constructor(code: string) {
|
|
13
|
+
this.code = code
|
|
14
|
+
}
|
|
15
|
+
toString() {
|
|
16
|
+
return this.code
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const isError = (v: unknown): v is FormulaError => v instanceof FormulaError
|
|
21
|
+
|
|
22
|
+
export type FormulaValue = number | string | boolean | null | FormulaError
|
|
23
|
+
type EvalValue = FormulaValue | FormulaValue[]
|
|
24
|
+
|
|
25
|
+
export interface FormulaContext {
|
|
26
|
+
/** Ligne (0-based) de la cellule qui contient la formule — pour les références par champ */
|
|
27
|
+
row: number
|
|
28
|
+
/** Résout une référence par nom de colonne (dans ctx.row). undefined = champ inconnu */
|
|
29
|
+
resolveField: (name: string) => FormulaValue | undefined
|
|
30
|
+
/** Résout une référence positionnelle A1 (row/col 0-based). undefined = hors grille */
|
|
31
|
+
resolveCell: (row: number, col: number) => FormulaValue | undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── AST ───
|
|
35
|
+
interface RefToken {
|
|
36
|
+
raw: string
|
|
37
|
+
field?: string // nom de colonne (identifiant simple)
|
|
38
|
+
col?: number // 0-based si A1
|
|
39
|
+
row?: number // 0-based si A1
|
|
40
|
+
colAbs?: boolean
|
|
41
|
+
rowAbs?: boolean
|
|
42
|
+
either?: boolean // ressemble à A1 (lettres+chiffres) mais peut être un champ (ex. price1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type Node =
|
|
46
|
+
| { kind: "num"; value: number }
|
|
47
|
+
| { kind: "str"; value: string }
|
|
48
|
+
| { kind: "ref"; ref: RefToken }
|
|
49
|
+
| { kind: "range"; start: RefToken; end: RefToken }
|
|
50
|
+
| { kind: "call"; name: string; args: Node[] }
|
|
51
|
+
| { kind: "unary"; op: "+" | "-"; arg: Node }
|
|
52
|
+
| { kind: "postfix"; op: "%"; arg: Node }
|
|
53
|
+
| { kind: "binary"; op: string; left: Node; right: Node }
|
|
54
|
+
|
|
55
|
+
// ─── Parser (descente récursive, précédence) ───
|
|
56
|
+
class Parser {
|
|
57
|
+
private pos = 0
|
|
58
|
+
|
|
59
|
+
constructor(private src: string) {}
|
|
60
|
+
|
|
61
|
+
private peek() {
|
|
62
|
+
return this.src[this.pos]
|
|
63
|
+
}
|
|
64
|
+
eof() {
|
|
65
|
+
return this.pos >= this.src.length
|
|
66
|
+
}
|
|
67
|
+
private next() {
|
|
68
|
+
return this.src[this.pos++]
|
|
69
|
+
}
|
|
70
|
+
private skipWs() {
|
|
71
|
+
while (!this.eof() && /\s/.test(this.peek()!)) this.pos++
|
|
72
|
+
}
|
|
73
|
+
private expect(char: string) {
|
|
74
|
+
this.skipWs()
|
|
75
|
+
if (this.peek() !== char) throw new Error("expected " + char)
|
|
76
|
+
this.pos++
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** parse une expression complète (niveau comparaison) */
|
|
80
|
+
parseExpr(): Node {
|
|
81
|
+
this.skipWs()
|
|
82
|
+
return this.parseComparison()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private parseComparison(): Node {
|
|
86
|
+
let left = this.parseAdditive()
|
|
87
|
+
for (;;) {
|
|
88
|
+
this.skipWs()
|
|
89
|
+
let op: string | null = null
|
|
90
|
+
if (this.peek() === "=") op = "="
|
|
91
|
+
else if (this.peek() === "<") {
|
|
92
|
+
if (this.src[this.pos + 1] === ">") {
|
|
93
|
+
op = "<>"
|
|
94
|
+
this.pos++
|
|
95
|
+
} else if (this.src[this.pos + 1] === "=") {
|
|
96
|
+
op = "<="
|
|
97
|
+
this.pos++
|
|
98
|
+
} else op = "<"
|
|
99
|
+
} else if (this.peek() === ">") {
|
|
100
|
+
if (this.src[this.pos + 1] === "=") {
|
|
101
|
+
op = ">="
|
|
102
|
+
this.pos++
|
|
103
|
+
} else op = ">"
|
|
104
|
+
}
|
|
105
|
+
if (!op) return left
|
|
106
|
+
this.pos++
|
|
107
|
+
const right = this.parseAdditive()
|
|
108
|
+
left = { kind: "binary", op, left, right }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private parseAdditive(): Node {
|
|
113
|
+
let left = this.parseMultiplicative()
|
|
114
|
+
for (;;) {
|
|
115
|
+
this.skipWs()
|
|
116
|
+
const op = this.peek()
|
|
117
|
+
if (op !== "+" && op !== "-" && op !== "&") return left
|
|
118
|
+
this.pos++
|
|
119
|
+
const right = this.parseMultiplicative()
|
|
120
|
+
left = { kind: "binary", op, left, right }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private parseMultiplicative(): Node {
|
|
125
|
+
let left = this.parsePower()
|
|
126
|
+
for (;;) {
|
|
127
|
+
this.skipWs()
|
|
128
|
+
const op = this.peek()
|
|
129
|
+
if (op !== "*" && op !== "/") return left
|
|
130
|
+
this.pos++
|
|
131
|
+
const right = this.parsePower()
|
|
132
|
+
left = { kind: "binary", op, left, right }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private parsePower(): Node {
|
|
137
|
+
const left = this.parseUnary()
|
|
138
|
+
this.skipWs()
|
|
139
|
+
if (this.peek() === "^") {
|
|
140
|
+
this.pos++
|
|
141
|
+
const right = this.parsePower() // associatif à droite
|
|
142
|
+
return { kind: "binary", op: "^", left, right }
|
|
143
|
+
}
|
|
144
|
+
return left
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private parseUnary(): Node {
|
|
148
|
+
this.skipWs()
|
|
149
|
+
const op = this.peek()
|
|
150
|
+
if (op === "+" || op === "-") {
|
|
151
|
+
this.pos++
|
|
152
|
+
return { kind: "unary", op, arg: this.parseUnary() }
|
|
153
|
+
}
|
|
154
|
+
return this.parsePostfix()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private parsePostfix(): Node {
|
|
158
|
+
let node = this.parsePrimary()
|
|
159
|
+
for (;;) {
|
|
160
|
+
this.skipWs()
|
|
161
|
+
if (this.peek() === "%") {
|
|
162
|
+
this.pos++
|
|
163
|
+
node = { kind: "postfix", op: "%", arg: node }
|
|
164
|
+
} else return node
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private parsePrimary(): Node {
|
|
169
|
+
this.skipWs()
|
|
170
|
+
const ch = this.peek()
|
|
171
|
+
if (ch === "(") {
|
|
172
|
+
this.pos++
|
|
173
|
+
const node = this.parseExpr()
|
|
174
|
+
this.expect(")")
|
|
175
|
+
return node
|
|
176
|
+
}
|
|
177
|
+
if (ch === '"' || ch === "'") return { kind: "str", value: this.parseString(ch) }
|
|
178
|
+
if (ch && /[0-9.]/.test(ch)) return this.parseNumber()
|
|
179
|
+
if (ch && /[A-Za-z_$]/.test(ch)) return this.parseRefOrCall()
|
|
180
|
+
throw new Error("unexpected char")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private parseString(quote: string): string {
|
|
184
|
+
this.pos++ // quote d'ouverture
|
|
185
|
+
let out = ""
|
|
186
|
+
while (!this.eof()) {
|
|
187
|
+
const c = this.next()
|
|
188
|
+
if (c === quote) {
|
|
189
|
+
if (this.peek() === quote) {
|
|
190
|
+
this.pos++ // échappement ""
|
|
191
|
+
out += quote
|
|
192
|
+
} else return out
|
|
193
|
+
} else out += c
|
|
194
|
+
}
|
|
195
|
+
throw new Error("unterminated string")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private parseNumber(): Node {
|
|
199
|
+
let s = ""
|
|
200
|
+
while (!this.eof() && /[0-9.]/.test(this.peek()!)) s += this.next()
|
|
201
|
+
const value = parseFloat(s)
|
|
202
|
+
return { kind: "num", value: Number.isNaN(value) ? 0 : value }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private parseRefOrCall(): Node {
|
|
206
|
+
// [$]lettres[$]chiffres → A1 ; identifiant( → appel ; sinon champ
|
|
207
|
+
let colAbs = false
|
|
208
|
+
if (this.peek() === "$") {
|
|
209
|
+
colAbs = true
|
|
210
|
+
this.pos++
|
|
211
|
+
}
|
|
212
|
+
let letters = ""
|
|
213
|
+
while (!this.eof() && /[A-Za-z]/.test(this.peek()!)) letters += this.next()
|
|
214
|
+
if (letters.length === 0) throw new Error("bad ref")
|
|
215
|
+
|
|
216
|
+
// Appel de fonction : identifiant suivi de "("
|
|
217
|
+
if (this.peek() === "(") {
|
|
218
|
+
if (colAbs) throw new Error("bad call")
|
|
219
|
+
this.pos++
|
|
220
|
+
const args: Node[] = []
|
|
221
|
+
this.skipWs()
|
|
222
|
+
if (this.peek() !== ")") {
|
|
223
|
+
for (;;) {
|
|
224
|
+
args.push(this.parseExpr())
|
|
225
|
+
this.skipWs()
|
|
226
|
+
if (this.peek() === ",") {
|
|
227
|
+
this.pos++
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
break
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
this.expect(")")
|
|
234
|
+
return { kind: "call", name: letters, args }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let rowAbs = false
|
|
238
|
+
if (this.peek() === "$") {
|
|
239
|
+
rowAbs = true
|
|
240
|
+
this.pos++
|
|
241
|
+
}
|
|
242
|
+
let digits = ""
|
|
243
|
+
while (!this.eof() && /[0-9]/.test(this.peek()!)) digits += this.next()
|
|
244
|
+
|
|
245
|
+
let token: RefToken
|
|
246
|
+
if (digits.length > 0 && letters.length <= 3) {
|
|
247
|
+
token = {
|
|
248
|
+
raw: (colAbs ? "$" : "") + letters + (rowAbs ? "$" : "") + digits,
|
|
249
|
+
col: lettersToCol(letters),
|
|
250
|
+
row: parseInt(digits, 10) - 1,
|
|
251
|
+
colAbs,
|
|
252
|
+
rowAbs,
|
|
253
|
+
either: !colAbs && !rowAbs,
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
token = { raw: (colAbs ? "$" : "") + letters, field: letters }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Plage A1:A3 (uniquement par positions)
|
|
260
|
+
this.skipWs()
|
|
261
|
+
if (this.peek() === ":") {
|
|
262
|
+
this.pos++
|
|
263
|
+
const end = this.parseRefOnly()
|
|
264
|
+
return { kind: "range", start: token, end }
|
|
265
|
+
}
|
|
266
|
+
return { kind: "ref", ref: token }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Lit un token de référence seul (extrémité de plage) */
|
|
270
|
+
private parseRefOnly(): RefToken {
|
|
271
|
+
this.skipWs()
|
|
272
|
+
let colAbs = false
|
|
273
|
+
if (this.peek() === "$") {
|
|
274
|
+
colAbs = true
|
|
275
|
+
this.pos++
|
|
276
|
+
}
|
|
277
|
+
let letters = ""
|
|
278
|
+
while (!this.eof() && /[A-Za-z]/.test(this.peek()!)) letters += this.next()
|
|
279
|
+
let rowAbs = false
|
|
280
|
+
if (this.peek() === "$") {
|
|
281
|
+
rowAbs = true
|
|
282
|
+
this.pos++
|
|
283
|
+
}
|
|
284
|
+
let digits = ""
|
|
285
|
+
while (!this.eof() && /[0-9]/.test(this.peek()!)) digits += this.next()
|
|
286
|
+
if (digits.length === 0 || letters.length === 0 || letters.length > 3)
|
|
287
|
+
throw new Error("bad range")
|
|
288
|
+
return {
|
|
289
|
+
raw: (colAbs ? "$" : "") + letters + (rowAbs ? "$" : "") + digits,
|
|
290
|
+
col: lettersToCol(letters),
|
|
291
|
+
row: parseInt(digits, 10) - 1,
|
|
292
|
+
colAbs,
|
|
293
|
+
rowAbs,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const lettersToCol = (letters: string): number => {
|
|
299
|
+
let n = 0
|
|
300
|
+
for (const ch of letters.toUpperCase()) n = n * 26 + (ch.charCodeAt(0) - 64)
|
|
301
|
+
return n - 1
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── Évaluation ───
|
|
305
|
+
const err = (code: string): FormulaError => new FormulaError(code)
|
|
306
|
+
|
|
307
|
+
const toNum = (v: FormulaValue): number | FormulaError => {
|
|
308
|
+
if (v === null || v === undefined) return 0
|
|
309
|
+
if (typeof v === "boolean") return v ? 1 : 0
|
|
310
|
+
if (typeof v === "number") return v
|
|
311
|
+
const t = (v as string).trim()
|
|
312
|
+
if (t === "") return 0
|
|
313
|
+
const n = Number(t)
|
|
314
|
+
return Number.isNaN(n) ? err("#VALUE!") : n
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const toStr = (v: FormulaValue): string => {
|
|
318
|
+
if (v === null || v === undefined) return ""
|
|
319
|
+
if (typeof v === "boolean") return v ? "TRUE" : "FALSE"
|
|
320
|
+
return String(v)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const truthy = (v: FormulaValue): boolean => {
|
|
324
|
+
if (v === null || v === undefined) return false
|
|
325
|
+
if (typeof v === "boolean") return v
|
|
326
|
+
if (typeof v === "number") return v !== 0
|
|
327
|
+
return v !== ""
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// collect=false ne produit jamais de tableau (les plages renvoient #VALUE!) —
|
|
331
|
+
// ce garde sert uniquement au typage.
|
|
332
|
+
const toScalar = (v: EvalValue): FormulaValue => (Array.isArray(v) ? err("#VALUE!") : v)
|
|
333
|
+
|
|
334
|
+
const resolveRef = (ref: RefToken, ctx: FormulaContext): FormulaValue => {
|
|
335
|
+
if (ref.field !== undefined) {
|
|
336
|
+
const v = ctx.resolveField(ref.field)
|
|
337
|
+
return v === undefined ? err("#NAME?") : v
|
|
338
|
+
}
|
|
339
|
+
if (ref.either) {
|
|
340
|
+
const f = ctx.resolveField(ref.raw)
|
|
341
|
+
if (f !== undefined) return f
|
|
342
|
+
}
|
|
343
|
+
const v = ctx.resolveCell(ref.row!, ref.col!)
|
|
344
|
+
return v === undefined ? err("#REF!") : v
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const resolveRefPos = (ref: RefToken): [number, number] | null => {
|
|
348
|
+
if (ref.field !== undefined) return null // pas de plage par champ
|
|
349
|
+
return [ref.row!, ref.col!]
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function evalNode(node: Node, ctx: FormulaContext, collect: boolean): EvalValue {
|
|
353
|
+
switch (node.kind) {
|
|
354
|
+
case "num":
|
|
355
|
+
return node.value
|
|
356
|
+
case "str":
|
|
357
|
+
return node.value
|
|
358
|
+
case "ref":
|
|
359
|
+
return resolveRef(node.ref, ctx)
|
|
360
|
+
case "range": {
|
|
361
|
+
if (!collect) return err("#VALUE!")
|
|
362
|
+
const a = resolveRefPos(node.start)
|
|
363
|
+
const b = resolveRefPos(node.end)
|
|
364
|
+
if (!a || !b) return err("#REF!")
|
|
365
|
+
const out: FormulaValue[] = []
|
|
366
|
+
for (let r = Math.min(a[0], b[0]); r <= Math.max(a[0], b[0]); r++)
|
|
367
|
+
for (let c = Math.min(a[1], b[1]); c <= Math.max(a[1], b[1]); c++)
|
|
368
|
+
out.push(ctx.resolveCell(r, c) ?? null)
|
|
369
|
+
return out
|
|
370
|
+
}
|
|
371
|
+
case "unary": {
|
|
372
|
+
const v = evalNode(node.arg, ctx, false)
|
|
373
|
+
if (isError(v)) return v
|
|
374
|
+
const n = toNum(v as FormulaValue)
|
|
375
|
+
return isError(n) ? n : node.op === "-" ? -n : n
|
|
376
|
+
}
|
|
377
|
+
case "postfix": {
|
|
378
|
+
const v = evalNode(node.arg, ctx, false)
|
|
379
|
+
if (isError(v)) return v
|
|
380
|
+
const n = toNum(v as FormulaValue)
|
|
381
|
+
return isError(n) ? n : n / 100
|
|
382
|
+
}
|
|
383
|
+
case "binary":
|
|
384
|
+
return evalBinary(node, ctx)
|
|
385
|
+
case "call":
|
|
386
|
+
return evalCall(node, ctx)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function evalBinary(node: Extract<Node, { kind: "binary" }>, ctx: FormulaContext): FormulaValue {
|
|
391
|
+
if (node.op === "&") {
|
|
392
|
+
const a = toScalar(evalNode(node.left, ctx, false))
|
|
393
|
+
const b = toScalar(evalNode(node.right, ctx, false))
|
|
394
|
+
if (isError(a)) return a
|
|
395
|
+
if (isError(b)) return b
|
|
396
|
+
return toStr(a) + toStr(b)
|
|
397
|
+
}
|
|
398
|
+
const a = toScalar(evalNode(node.left, ctx, false))
|
|
399
|
+
const b = toScalar(evalNode(node.right, ctx, false))
|
|
400
|
+
if (isError(a)) return a
|
|
401
|
+
if (isError(b)) return b
|
|
402
|
+
|
|
403
|
+
switch (node.op) {
|
|
404
|
+
case "+":
|
|
405
|
+
case "-":
|
|
406
|
+
case "*":
|
|
407
|
+
case "/":
|
|
408
|
+
case "^": {
|
|
409
|
+
const na = toNum(a as FormulaValue)
|
|
410
|
+
const nb = toNum(b as FormulaValue)
|
|
411
|
+
if (isError(na)) return na
|
|
412
|
+
if (isError(nb)) return nb
|
|
413
|
+
switch (node.op) {
|
|
414
|
+
case "+":
|
|
415
|
+
return na + nb
|
|
416
|
+
case "-":
|
|
417
|
+
return na - nb
|
|
418
|
+
case "*":
|
|
419
|
+
return na * nb
|
|
420
|
+
case "/":
|
|
421
|
+
return nb === 0 ? err("#DIV/0!") : na / nb
|
|
422
|
+
case "^":
|
|
423
|
+
return Math.pow(na, nb)
|
|
424
|
+
}
|
|
425
|
+
break
|
|
426
|
+
}
|
|
427
|
+
case "=":
|
|
428
|
+
case "<>":
|
|
429
|
+
case "<":
|
|
430
|
+
case ">":
|
|
431
|
+
case "<=":
|
|
432
|
+
case ">=": {
|
|
433
|
+
const cmp = compare(a as FormulaValue, b as FormulaValue)
|
|
434
|
+
if (cmp === "err") return err("#VALUE!")
|
|
435
|
+
switch (node.op) {
|
|
436
|
+
case "=":
|
|
437
|
+
return cmp === 0
|
|
438
|
+
case "<>":
|
|
439
|
+
return cmp !== 0
|
|
440
|
+
case "<":
|
|
441
|
+
return cmp < 0
|
|
442
|
+
case ">":
|
|
443
|
+
return cmp > 0
|
|
444
|
+
case "<=":
|
|
445
|
+
return cmp <= 0
|
|
446
|
+
case ">=":
|
|
447
|
+
return cmp >= 0
|
|
448
|
+
}
|
|
449
|
+
break
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return err("#ERROR!")
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function compare(a: FormulaValue, b: FormulaValue): number | "err" {
|
|
456
|
+
if (typeof a === "boolean") a = a ? 1 : 0
|
|
457
|
+
if (typeof b === "boolean") b = b ? 1 : 0
|
|
458
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
459
|
+
return a === b ? 0 : a < b ? -1 : 1
|
|
460
|
+
}
|
|
461
|
+
if (a === null) a = 0
|
|
462
|
+
if (b === null) b = 0
|
|
463
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
464
|
+
return a === b ? 0 : a < b ? -1 : 1
|
|
465
|
+
}
|
|
466
|
+
const na = toNum(a)
|
|
467
|
+
const nb = toNum(b)
|
|
468
|
+
if (!isError(na) && !isError(nb)) {
|
|
469
|
+
return na === nb ? 0 : na < nb ? -1 : 1
|
|
470
|
+
}
|
|
471
|
+
const sa = toStr(a)
|
|
472
|
+
const sb = toStr(b)
|
|
473
|
+
return sa === sb ? 0 : sa < sb ? -1 : 1
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function evalCall(node: Extract<Node, { kind: "call" }>, ctx: FormulaContext): FormulaValue {
|
|
477
|
+
const name = node.name.toLowerCase()
|
|
478
|
+
|
|
479
|
+
// Appels paresseux (évalués conditionnellement)
|
|
480
|
+
if (name === "if" && node.args.length >= 2) {
|
|
481
|
+
const cond = toScalar(evalNode(node.args[0]!, ctx, false))
|
|
482
|
+
if (isError(cond)) return cond
|
|
483
|
+
return truthy(cond)
|
|
484
|
+
? toScalar(evalNode(node.args[1]!, ctx, false))
|
|
485
|
+
: node.args[2] !== undefined
|
|
486
|
+
? toScalar(evalNode(node.args[2], ctx, false))
|
|
487
|
+
: false
|
|
488
|
+
}
|
|
489
|
+
if (name === "iferror" && node.args.length >= 1) {
|
|
490
|
+
const v = toScalar(evalNode(node.args[0]!, ctx, false))
|
|
491
|
+
if (isError(v))
|
|
492
|
+
return node.args[1] !== undefined ? toScalar(evalNode(node.args[1], ctx, false)) : null
|
|
493
|
+
return v
|
|
494
|
+
}
|
|
495
|
+
if (name === "and" || name === "or") {
|
|
496
|
+
if (node.args.length === 0) return name === "and"
|
|
497
|
+
for (const arg of node.args) {
|
|
498
|
+
const v = toScalar(evalNode(arg, ctx, false))
|
|
499
|
+
if (isError(v)) return v
|
|
500
|
+
const t = truthy(v)
|
|
501
|
+
if (name === "and" && !t) return false
|
|
502
|
+
if (name === "or" && t) return true
|
|
503
|
+
}
|
|
504
|
+
return name === "and"
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const fn = FUNCTIONS[name]
|
|
508
|
+
if (!fn) return err("#NAME?")
|
|
509
|
+
|
|
510
|
+
const raw = node.args.map((a) => evalNode(a, ctx, true))
|
|
511
|
+
const hadArray = raw.some(Array.isArray)
|
|
512
|
+
const flat = raw.flatMap((v) => (Array.isArray(v) ? (v as FormulaValue[]) : [v as FormulaValue]))
|
|
513
|
+
return fn(flat, hadArray)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
type GridFunction = (args: FormulaValue[], hadArray: boolean) => FormulaValue
|
|
517
|
+
|
|
518
|
+
const scalar = (args: FormulaValue[], hadArray: boolean, fn: (v: FormulaValue) => FormulaValue): FormulaValue => {
|
|
519
|
+
if (hadArray) return err("#VALUE!")
|
|
520
|
+
return fn(args[0] ?? null)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const numbers = (args: FormulaValue[]): number[] =>
|
|
524
|
+
args.filter((v): v is number => typeof v === "number")
|
|
525
|
+
|
|
526
|
+
const FUNCTIONS: Record<string, GridFunction> = {
|
|
527
|
+
sum: (args) => numbers(args).reduce((acc, n) => acc + n, 0),
|
|
528
|
+
average: (args) => {
|
|
529
|
+
const n = numbers(args)
|
|
530
|
+
return n.length ? n.reduce((acc, x) => acc + x, 0) / n.length : err("#DIV/0!")
|
|
531
|
+
},
|
|
532
|
+
avg: (args) => FUNCTIONS.average!(args, false),
|
|
533
|
+
min: (args) => {
|
|
534
|
+
const n = numbers(args)
|
|
535
|
+
return n.length ? Math.min(...n) : 0
|
|
536
|
+
},
|
|
537
|
+
max: (args) => {
|
|
538
|
+
const n = numbers(args)
|
|
539
|
+
return n.length ? Math.max(...n) : 0
|
|
540
|
+
},
|
|
541
|
+
count: (args) => numbers(args).length,
|
|
542
|
+
counta: (args) => args.filter((v) => v !== null && v !== undefined).length,
|
|
543
|
+
abs: (args, h) => scalar(args, h, (v) => {
|
|
544
|
+
const n = toNum(v)
|
|
545
|
+
return isError(n) ? n : Math.abs(n)
|
|
546
|
+
}),
|
|
547
|
+
round: (args, h) => scalar(args, h, (v) => {
|
|
548
|
+
const n = toNum(v)
|
|
549
|
+
if (isError(n)) return n
|
|
550
|
+
const d = args[1] === undefined ? 0 : Math.trunc(Number(args[1]) || 0)
|
|
551
|
+
const f = Math.pow(10, d)
|
|
552
|
+
return Math.round(n * f) / f
|
|
553
|
+
}),
|
|
554
|
+
roundup: (args, h) => scalar(args, h, (v) => {
|
|
555
|
+
const n = toNum(v)
|
|
556
|
+
if (isError(n)) return n
|
|
557
|
+
const d = args[1] === undefined ? 0 : Math.trunc(Number(args[1]) || 0)
|
|
558
|
+
const f = Math.pow(10, d)
|
|
559
|
+
return (n < 0 ? -1 : 1) * Math.ceil(Math.abs(n) * f) / f
|
|
560
|
+
}),
|
|
561
|
+
rounddown: (args, h) => scalar(args, h, (v) => {
|
|
562
|
+
const n = toNum(v)
|
|
563
|
+
if (isError(n)) return n
|
|
564
|
+
const d = args[1] === undefined ? 0 : Math.trunc(Number(args[1]) || 0)
|
|
565
|
+
const f = Math.pow(10, d)
|
|
566
|
+
return (n < 0 ? -1 : 1) * Math.floor(Math.abs(n) * f) / f
|
|
567
|
+
}),
|
|
568
|
+
floor: (args, h) => scalar(args, h, (v) => {
|
|
569
|
+
const n = toNum(v)
|
|
570
|
+
return isError(n) ? n : Math.floor(n)
|
|
571
|
+
}),
|
|
572
|
+
ceiling: (args, h) => scalar(args, h, (v) => {
|
|
573
|
+
const n = toNum(v)
|
|
574
|
+
return isError(n) ? n : Math.ceil(n)
|
|
575
|
+
}),
|
|
576
|
+
ceil: (args, h) => FUNCTIONS.ceiling!(args, h),
|
|
577
|
+
int: (args, h) => scalar(args, h, (v) => {
|
|
578
|
+
const n = toNum(v)
|
|
579
|
+
return isError(n) ? n : Math.trunc(n)
|
|
580
|
+
}),
|
|
581
|
+
mod: (args, h) => scalar(args, h, (v) => {
|
|
582
|
+
const n = toNum(v)
|
|
583
|
+
if (isError(n)) return n
|
|
584
|
+
const m = toNum(args[1] ?? null)
|
|
585
|
+
if (isError(m)) return m
|
|
586
|
+
return m === 0 ? err("#DIV/0!") : n % m
|
|
587
|
+
}),
|
|
588
|
+
sqrt: (args, h) => scalar(args, h, (v) => {
|
|
589
|
+
const n = toNum(v)
|
|
590
|
+
return isError(n) ? n : n < 0 ? err("#VALUE!") : Math.sqrt(n)
|
|
591
|
+
}),
|
|
592
|
+
power: (args, h) => scalar(args, h, (v) => {
|
|
593
|
+
const n = toNum(v)
|
|
594
|
+
if (isError(n)) return n
|
|
595
|
+
const e = toNum(args[1] ?? null)
|
|
596
|
+
return isError(e) ? e : Math.pow(n, e)
|
|
597
|
+
}),
|
|
598
|
+
pow: (args, h) => FUNCTIONS.power!(args, h),
|
|
599
|
+
pi: () => Math.PI,
|
|
600
|
+
not: (args, h) => scalar(args, h, (v) => !truthy(v)),
|
|
601
|
+
concat: (args) => args.map(toStr).join(""),
|
|
602
|
+
upper: (args, h) => scalar(args, h, (v) => toStr(v).toUpperCase()),
|
|
603
|
+
lower: (args, h) => scalar(args, h, (v) => toStr(v).toLowerCase()),
|
|
604
|
+
trim: (args, h) => scalar(args, h, (v) => toStr(v).trim()),
|
|
605
|
+
len: (args, h) => scalar(args, h, (v) => toStr(v).length),
|
|
606
|
+
isblank: (args, h) => scalar(args, h, (v) => v === null || v === undefined || v === ""),
|
|
607
|
+
isnumber: (args, h) => scalar(args, h, (v) => typeof v === "number"),
|
|
608
|
+
istext: (args, h) => scalar(args, h, (v) => typeof v === "string"),
|
|
609
|
+
value: (args, h) => scalar(args, h, (v) => toNum(v)),
|
|
610
|
+
n: (args, h) => scalar(args, h, (v) => {
|
|
611
|
+
if (v === null || v === undefined) return 0
|
|
612
|
+
if (typeof v === "boolean") return v ? 1 : 0
|
|
613
|
+
if (typeof v === "number") return v
|
|
614
|
+
return toNum(v)
|
|
615
|
+
}),
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Évalue une formule ("=…" optionnel). Retourne une valeur ou une FormulaError. */
|
|
619
|
+
export function evaluateFormula(source: string, ctx: FormulaContext): FormulaValue {
|
|
620
|
+
const src = source.startsWith("=") ? source.slice(1) : source
|
|
621
|
+
try {
|
|
622
|
+
const parser = new Parser(src)
|
|
623
|
+
const node = parser.parseExpr()
|
|
624
|
+
if (!parser.eof()) return err("#ERROR!")
|
|
625
|
+
const v = evalNode(node, ctx, false)
|
|
626
|
+
return Array.isArray(v) ? err("#VALUE!") : v
|
|
627
|
+
} catch {
|
|
628
|
+
return err("#ERROR!")
|
|
629
|
+
}
|
|
630
|
+
}
|
package/lib/icons.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Icônes par défaut du design system — noms Iconify (famille lucide:),
|
|
2
|
+
// rendus par le composant <Icon> de @iconify/vue (https://iconify.design/docs/icon-components/vue/).
|
|
3
|
+
// Source unique des icônes internes : un composant peut exposer une prop
|
|
4
|
+
// d'icône publique (string) qui surcharge ces défauts.
|
|
5
|
+
export const icons = {
|
|
6
|
+
arrowDown: "lucide:arrow-down",
|
|
7
|
+
arrowUp: "lucide:arrow-up",
|
|
8
|
+
arrowUpDown: "lucide:arrow-up-down",
|
|
9
|
+
calendarDays: "lucide:calendar-days",
|
|
10
|
+
check: "lucide:check",
|
|
11
|
+
chevronDown: "lucide:chevron-down",
|
|
12
|
+
chevronLeft: "lucide:chevron-left",
|
|
13
|
+
chevronRight: "lucide:chevron-right",
|
|
14
|
+
chevronUp: "lucide:chevron-up",
|
|
15
|
+
chevronsLeft: "lucide:chevrons-left",
|
|
16
|
+
chevronsRight: "lucide:chevrons-right",
|
|
17
|
+
copy: "lucide:copy",
|
|
18
|
+
eye: "lucide:eye",
|
|
19
|
+
eyeOff: "lucide:eye-off",
|
|
20
|
+
file: "lucide:file",
|
|
21
|
+
fileArchive: "lucide:file-archive",
|
|
22
|
+
fileAudio: "lucide:file-audio",
|
|
23
|
+
fileCode: "lucide:file-code",
|
|
24
|
+
fileImage: "lucide:file-image",
|
|
25
|
+
filePlus: "lucide:file-plus",
|
|
26
|
+
fileSpreadsheet: "lucide:file-spreadsheet",
|
|
27
|
+
fileText: "lucide:file-text",
|
|
28
|
+
fileVideo: "lucide:file-video",
|
|
29
|
+
gripVertical: "lucide:grip-vertical",
|
|
30
|
+
imagePlus: "lucide:image-plus",
|
|
31
|
+
menu: "lucide:menu",
|
|
32
|
+
minus: "lucide:minus",
|
|
33
|
+
plus: "lucide:plus",
|
|
34
|
+
refreshCw: "lucide:refresh-cw",
|
|
35
|
+
search: "lucide:search",
|
|
36
|
+
star: "lucide:star",
|
|
37
|
+
x: "lucide:x",
|
|
38
|
+
} as const
|