@nodaro/shared 1.4.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/LICENSE +202 -0
- package/README.md +32 -0
- package/dist/index.cjs +12506 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +12269 -0
- package/dist/index.d.ts +12269 -0
- package/dist/index.js +11831 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
- package/src/__tests__/ai-avatar-pricing.test.ts +321 -0
- package/src/__tests__/apply-video-negative-prompt.test.ts +76 -0
- package/src/__tests__/attach-columns.test.ts +28 -0
- package/src/__tests__/audio-crossfade-curves.test.ts +33 -0
- package/src/__tests__/build-model-menu.test.ts +58 -0
- package/src/__tests__/character-aspect-defaults.test.ts +142 -0
- package/src/__tests__/character-mention-slug.test.ts +199 -0
- package/src/__tests__/character-reference-photo-kinds.test.ts +32 -0
- package/src/__tests__/character-usage-mode.test.ts +147 -0
- package/src/__tests__/character-variant-assets.test.ts +142 -0
- package/src/__tests__/character-voice.test.ts +130 -0
- package/src/__tests__/cinematic-avatar-pricing.test.ts +119 -0
- package/src/__tests__/combine-transitions.test.ts +137 -0
- package/src/__tests__/component-types.test.ts +87 -0
- package/src/__tests__/condition-variables.test.ts +98 -0
- package/src/__tests__/credit-identifiers.test.ts +811 -0
- package/src/__tests__/default-video-provider.test.ts +63 -0
- package/src/__tests__/entity-image-handle.test.ts +43 -0
- package/src/__tests__/entity-variants.test.ts +33 -0
- package/src/__tests__/fal-lip-sync-sets.test.ts +31 -0
- package/src/__tests__/featured-entities.test.ts +33 -0
- package/src/__tests__/film-pricing.test.ts +64 -0
- package/src/__tests__/filter-condition.test.ts +285 -0
- package/src/__tests__/freecut-protocol.test.ts +35 -0
- package/src/__tests__/generate-text-items.test.ts +19 -0
- package/src/__tests__/generated-results.test.ts +50 -0
- package/src/__tests__/graph-utils.test.ts +618 -0
- package/src/__tests__/group-aggregation.test.ts +83 -0
- package/src/__tests__/i18n-locale-completeness.test.ts +156 -0
- package/src/__tests__/image-critic-node.test.ts +86 -0
- package/src/__tests__/image-mask-mode.test.ts +16 -0
- package/src/__tests__/image-ref-limit.test.ts +78 -0
- package/src/__tests__/inpaint-region.test.ts +24 -0
- package/src/__tests__/input-field-map-parity.test.ts +30 -0
- package/src/__tests__/json-evaluator.test.ts +289 -0
- package/src/__tests__/json-path.test.ts +64 -0
- package/src/__tests__/lip-sync-pricing.test.ts +142 -0
- package/src/__tests__/list-merge.test.ts +96 -0
- package/src/__tests__/list-sort.test.ts +134 -0
- package/src/__tests__/llm-models.test.ts +445 -0
- package/src/__tests__/location-mention-slug.test.ts +246 -0
- package/src/__tests__/locations-coverage-critic-schema.test.ts +92 -0
- package/src/__tests__/loop-delimiter.test.ts +39 -0
- package/src/__tests__/lottie-slots.test.ts +245 -0
- package/src/__tests__/model-catalog-flux2-resolutions.test.ts +21 -0
- package/src/__tests__/model-constants-location.test.ts +20 -0
- package/src/__tests__/model-constants.test.ts +122 -0
- package/src/__tests__/model-tree.test.ts +86 -0
- package/src/__tests__/no-internal-imports.test.ts +25 -0
- package/src/__tests__/node-default-mappings.test.ts +175 -0
- package/src/__tests__/node-mappable-fields.test.ts +7 -0
- package/src/__tests__/node-preset-export.test.ts +46 -0
- package/src/__tests__/node-preset-extract.test.ts +166 -0
- package/src/__tests__/node-refs-canonical.test.ts +54 -0
- package/src/__tests__/node-refs-combine.test.ts +54 -0
- package/src/__tests__/node-refs.test.ts +187 -0
- package/src/__tests__/node-runtime-keys.test.ts +99 -0
- package/src/__tests__/object-aspect-defaults.test.ts +59 -0
- package/src/__tests__/parameter-node-value.test.ts +137 -0
- package/src/__tests__/pinterest-url.test.ts +35 -0
- package/src/__tests__/pipeline-chat.test.ts +110 -0
- package/src/__tests__/pipeline-validation.test.ts +98 -0
- package/src/__tests__/presentation-utils.test.ts +1137 -0
- package/src/__tests__/producer-types.test.ts +47 -0
- package/src/__tests__/prompt-length-limits.test.ts +156 -0
- package/src/__tests__/provider-directive-defaults.test.ts +46 -0
- package/src/__tests__/reduce-strategy-registry.test.ts +43 -0
- package/src/__tests__/reference-board-templates.test.ts +33 -0
- package/src/__tests__/reference-roles.test.ts +36 -0
- package/src/__tests__/resolve-field-mappings.test.ts +110 -0
- package/src/__tests__/resolve-pipeline-model.test.ts +76 -0
- package/src/__tests__/scene-node-types.test.ts +80 -0
- package/src/__tests__/scraper-actors.test.ts +67 -0
- package/src/__tests__/seedance-2-catalog.test.ts +18 -0
- package/src/__tests__/seedance-2-credit-id.test.ts +23 -0
- package/src/__tests__/seedance-aspects.test.ts +52 -0
- package/src/__tests__/selector.test.ts +1151 -0
- package/src/__tests__/settled-with-limit.test.ts +214 -0
- package/src/__tests__/social-media-specs.test.ts +129 -0
- package/src/__tests__/sub-workflow-validation.test.ts +88 -0
- package/src/__tests__/switchx-pricing.test.ts +28 -0
- package/src/__tests__/to-connected-references.test.ts +153 -0
- package/src/__tests__/utilities.test.ts +541 -0
- package/src/__tests__/video-analysis-pricing.test.ts +58 -0
- package/src/__tests__/video-analysis.test.ts +63 -0
- package/src/__tests__/video-audio-capability.test.ts +204 -0
- package/src/__tests__/video-gen-providers.test.ts +28 -0
- package/src/__tests__/video-mode-aliases.test.ts +150 -0
- package/src/__tests__/video-ref-limits.test.ts +72 -0
- package/src/__tests__/video-utils.test.ts +303 -0
- package/src/__tests__/voice-changer-models.test.ts +33 -0
- package/src/__tests__/workflow-export-strip.test.ts +46 -0
- package/src/ai-avatar-pricing.ts +213 -0
- package/src/ancestor-refs.ts +48 -0
- package/src/animals.ts +231 -0
- package/src/audio-crossfade-curves.ts +73 -0
- package/src/audio-fx-presets.ts +64 -0
- package/src/avatar-payload-validators.ts +214 -0
- package/src/caption-styles.ts +31 -0
- package/src/character-aspect-defaults.ts +102 -0
- package/src/character-facets.ts +37 -0
- package/src/character-mention-slug.ts +166 -0
- package/src/character-usage-mode.ts +109 -0
- package/src/character-variant-assets.ts +121 -0
- package/src/character-voice.ts +151 -0
- package/src/cinematic-avatar-pricing.ts +111 -0
- package/src/clone-utils.ts +46 -0
- package/src/combine-transitions.ts +482 -0
- package/src/community.ts +112 -0
- package/src/component-types.ts +127 -0
- package/src/condition-variables.ts +65 -0
- package/src/credit-estimators/index.ts +15 -0
- package/src/credit-estimators/video-utils.ts +165 -0
- package/src/credit-identifiers.ts +304 -0
- package/src/entity-approval-types.ts +31 -0
- package/src/entity-aspect-defaults.ts +82 -0
- package/src/entity-asset-types.ts +296 -0
- package/src/entity-image-handle.ts +36 -0
- package/src/extra-refs.ts +139 -0
- package/src/featured-entities.ts +136 -0
- package/src/film-pricing.ts +117 -0
- package/src/filter-condition.ts +250 -0
- package/src/flux2-pricing.ts +18 -0
- package/src/freecut-protocol.ts +52 -0
- package/src/furniture.ts +167 -0
- package/src/generate-text-items.ts +15 -0
- package/src/generated-results.ts +63 -0
- package/src/group-aggregation.ts +75 -0
- package/src/hint-graph-types.ts +18 -0
- package/src/i18n/action-fx.ar.ts +89 -0
- package/src/i18n/action-fx.de.ts +89 -0
- package/src/i18n/action-fx.es.ts +89 -0
- package/src/i18n/action-fx.fr.ts +89 -0
- package/src/i18n/action-fx.he.ts +89 -0
- package/src/i18n/action-fx.hi.ts +89 -0
- package/src/i18n/action-fx.ja.ts +89 -0
- package/src/i18n/action-fx.ko.ts +89 -0
- package/src/i18n/action-fx.pt-BR.ts +89 -0
- package/src/i18n/action-fx.ru.ts +89 -0
- package/src/i18n/action-fx.zh-CN.ts +89 -0
- package/src/i18n/aesthetic.ar.ts +170 -0
- package/src/i18n/aesthetic.de.ts +48 -0
- package/src/i18n/aesthetic.es.ts +169 -0
- package/src/i18n/aesthetic.fr.ts +52 -0
- package/src/i18n/aesthetic.he.ts +169 -0
- package/src/i18n/aesthetic.hi.ts +55 -0
- package/src/i18n/aesthetic.ja.ts +165 -0
- package/src/i18n/aesthetic.ko.ts +169 -0
- package/src/i18n/aesthetic.pt-BR.ts +148 -0
- package/src/i18n/aesthetic.ru.ts +155 -0
- package/src/i18n/aesthetic.zh-CN.ts +172 -0
- package/src/i18n/animals.ar.ts +145 -0
- package/src/i18n/animals.de.ts +144 -0
- package/src/i18n/animals.es.ts +158 -0
- package/src/i18n/animals.fr.ts +144 -0
- package/src/i18n/animals.he.ts +155 -0
- package/src/i18n/animals.hi.ts +157 -0
- package/src/i18n/animals.ja.ts +159 -0
- package/src/i18n/animals.ko.ts +159 -0
- package/src/i18n/animals.pt-BR.ts +157 -0
- package/src/i18n/animals.ru.ts +155 -0
- package/src/i18n/animals.zh-CN.ts +155 -0
- package/src/i18n/atmosphere.ar.ts +49 -0
- package/src/i18n/atmosphere.de.ts +47 -0
- package/src/i18n/atmosphere.es.ts +50 -0
- package/src/i18n/atmosphere.fr.ts +47 -0
- package/src/i18n/atmosphere.he.ts +47 -0
- package/src/i18n/atmosphere.hi.ts +49 -0
- package/src/i18n/atmosphere.ja.ts +49 -0
- package/src/i18n/atmosphere.ko.ts +49 -0
- package/src/i18n/atmosphere.pt-BR.ts +49 -0
- package/src/i18n/atmosphere.ru.ts +47 -0
- package/src/i18n/atmosphere.zh-CN.ts +152 -0
- package/src/i18n/backdrop.ar.ts +53 -0
- package/src/i18n/backdrop.de.ts +46 -0
- package/src/i18n/backdrop.es.ts +54 -0
- package/src/i18n/backdrop.fr.ts +52 -0
- package/src/i18n/backdrop.he.ts +51 -0
- package/src/i18n/backdrop.hi.ts +59 -0
- package/src/i18n/backdrop.ja.ts +61 -0
- package/src/i18n/backdrop.ko.ts +59 -0
- package/src/i18n/backdrop.pt-BR.ts +59 -0
- package/src/i18n/backdrop.ru.ts +57 -0
- package/src/i18n/backdrop.zh-CN.ts +57 -0
- package/src/i18n/camera-format.ar.ts +40 -0
- package/src/i18n/camera-format.de.ts +37 -0
- package/src/i18n/camera-format.es.ts +41 -0
- package/src/i18n/camera-format.fr.ts +37 -0
- package/src/i18n/camera-format.he.ts +37 -0
- package/src/i18n/camera-format.hi.ts +42 -0
- package/src/i18n/camera-format.ja.ts +37 -0
- package/src/i18n/camera-format.ko.ts +37 -0
- package/src/i18n/camera-format.pt-BR.ts +46 -0
- package/src/i18n/camera-format.ru.ts +42 -0
- package/src/i18n/camera-format.zh-CN.ts +42 -0
- package/src/i18n/camera-motions.ar.ts +86 -0
- package/src/i18n/camera-motions.de.ts +74 -0
- package/src/i18n/camera-motions.es.ts +86 -0
- package/src/i18n/camera-motions.fr.ts +86 -0
- package/src/i18n/camera-motions.he.ts +86 -0
- package/src/i18n/camera-motions.hi.ts +99 -0
- package/src/i18n/camera-motions.ja.ts +99 -0
- package/src/i18n/camera-motions.ko.ts +99 -0
- package/src/i18n/camera-motions.pt-BR.ts +99 -0
- package/src/i18n/camera-motions.ru.ts +99 -0
- package/src/i18n/camera-motions.zh-CN.ts +99 -0
- package/src/i18n/character-fx.ar.ts +72 -0
- package/src/i18n/character-fx.de.ts +72 -0
- package/src/i18n/character-fx.es.ts +72 -0
- package/src/i18n/character-fx.fr.ts +72 -0
- package/src/i18n/character-fx.he.ts +72 -0
- package/src/i18n/character-fx.hi.ts +72 -0
- package/src/i18n/character-fx.ja.ts +72 -0
- package/src/i18n/character-fx.ko.ts +72 -0
- package/src/i18n/character-fx.pt-BR.ts +72 -0
- package/src/i18n/character-fx.ru.ts +72 -0
- package/src/i18n/character-fx.zh-CN.ts +72 -0
- package/src/i18n/color-look.ar.ts +50 -0
- package/src/i18n/color-look.de.ts +47 -0
- package/src/i18n/color-look.es.ts +50 -0
- package/src/i18n/color-look.fr.ts +50 -0
- package/src/i18n/color-look.he.ts +49 -0
- package/src/i18n/color-look.hi.ts +52 -0
- package/src/i18n/color-look.ja.ts +54 -0
- package/src/i18n/color-look.ko.ts +54 -0
- package/src/i18n/color-look.pt-BR.ts +54 -0
- package/src/i18n/color-look.ru.ts +52 -0
- package/src/i18n/color-look.zh-CN.ts +53 -0
- package/src/i18n/composition-effects.ar.ts +26 -0
- package/src/i18n/composition-effects.de.ts +25 -0
- package/src/i18n/composition-effects.es.ts +27 -0
- package/src/i18n/composition-effects.fr.ts +25 -0
- package/src/i18n/composition-effects.he.ts +25 -0
- package/src/i18n/composition-effects.hi.ts +27 -0
- package/src/i18n/composition-effects.ja.ts +27 -0
- package/src/i18n/composition-effects.ko.ts +27 -0
- package/src/i18n/composition-effects.pt-BR.ts +25 -0
- package/src/i18n/composition-effects.ru.ts +82 -0
- package/src/i18n/composition-effects.zh-CN.ts +25 -0
- package/src/i18n/era.ar.ts +37 -0
- package/src/i18n/era.de.ts +34 -0
- package/src/i18n/era.es.ts +37 -0
- package/src/i18n/era.fr.ts +37 -0
- package/src/i18n/era.he.ts +36 -0
- package/src/i18n/era.hi.ts +39 -0
- package/src/i18n/era.ja.ts +41 -0
- package/src/i18n/era.ko.ts +39 -0
- package/src/i18n/era.pt-BR.ts +41 -0
- package/src/i18n/era.ru.ts +121 -0
- package/src/i18n/era.zh-CN.ts +39 -0
- package/src/i18n/exposure-settings.ar.ts +28 -0
- package/src/i18n/exposure-settings.de.ts +26 -0
- package/src/i18n/exposure-settings.es.ts +28 -0
- package/src/i18n/exposure-settings.fr.ts +29 -0
- package/src/i18n/exposure-settings.he.ts +28 -0
- package/src/i18n/exposure-settings.hi.ts +31 -0
- package/src/i18n/exposure-settings.ja.ts +31 -0
- package/src/i18n/exposure-settings.ko.ts +31 -0
- package/src/i18n/exposure-settings.pt-BR.ts +31 -0
- package/src/i18n/exposure-settings.ru.ts +31 -0
- package/src/i18n/exposure-settings.zh-CN.ts +31 -0
- package/src/i18n/framing.ar.ts +78 -0
- package/src/i18n/framing.de.ts +73 -0
- package/src/i18n/framing.es.ts +78 -0
- package/src/i18n/framing.fr.ts +78 -0
- package/src/i18n/framing.he.ts +77 -0
- package/src/i18n/framing.hi.ts +82 -0
- package/src/i18n/framing.ja.ts +84 -0
- package/src/i18n/framing.ko.ts +82 -0
- package/src/i18n/framing.pt-BR.ts +84 -0
- package/src/i18n/framing.ru.ts +82 -0
- package/src/i18n/framing.zh-CN.ts +82 -0
- package/src/i18n/furniture.ar.ts +92 -0
- package/src/i18n/furniture.de.ts +93 -0
- package/src/i18n/furniture.es.ts +101 -0
- package/src/i18n/furniture.fr.ts +93 -0
- package/src/i18n/furniture.he.ts +101 -0
- package/src/i18n/furniture.hi.ts +101 -0
- package/src/i18n/furniture.ja.ts +101 -0
- package/src/i18n/furniture.ko.ts +101 -0
- package/src/i18n/furniture.pt-BR.ts +101 -0
- package/src/i18n/furniture.ru.ts +101 -0
- package/src/i18n/furniture.zh-CN.ts +101 -0
- package/src/i18n/held-prop.ar.ts +74 -0
- package/src/i18n/held-prop.de.ts +65 -0
- package/src/i18n/held-prop.es.ts +83 -0
- package/src/i18n/held-prop.fr.ts +74 -0
- package/src/i18n/held-prop.he.ts +73 -0
- package/src/i18n/held-prop.hi.ts +82 -0
- package/src/i18n/held-prop.ja.ts +84 -0
- package/src/i18n/held-prop.ko.ts +84 -0
- package/src/i18n/held-prop.pt-BR.ts +84 -0
- package/src/i18n/held-prop.ru.ts +82 -0
- package/src/i18n/held-prop.zh-CN.ts +82 -0
- package/src/i18n/index.ts +208 -0
- package/src/i18n/instrumentation.ar.ts +195 -0
- package/src/i18n/instrumentation.de.ts +195 -0
- package/src/i18n/instrumentation.es.ts +195 -0
- package/src/i18n/instrumentation.fr.ts +195 -0
- package/src/i18n/instrumentation.he.ts +195 -0
- package/src/i18n/instrumentation.hi.ts +195 -0
- package/src/i18n/instrumentation.ja.ts +195 -0
- package/src/i18n/instrumentation.ko.ts +195 -0
- package/src/i18n/instrumentation.pt-BR.ts +195 -0
- package/src/i18n/instrumentation.ru.ts +195 -0
- package/src/i18n/instrumentation.zh-CN.ts +195 -0
- package/src/i18n/lens.ar.ts +24 -0
- package/src/i18n/lens.de.ts +24 -0
- package/src/i18n/lens.es.ts +24 -0
- package/src/i18n/lens.fr.ts +24 -0
- package/src/i18n/lens.he.ts +24 -0
- package/src/i18n/lens.hi.ts +24 -0
- package/src/i18n/lens.ja.ts +24 -0
- package/src/i18n/lens.ko.ts +24 -0
- package/src/i18n/lens.pt-BR.ts +24 -0
- package/src/i18n/lens.ru.ts +25 -0
- package/src/i18n/lens.zh-CN.ts +24 -0
- package/src/i18n/lighting.ar.ts +86 -0
- package/src/i18n/lighting.de.ts +84 -0
- package/src/i18n/lighting.es.ts +89 -0
- package/src/i18n/lighting.fr.ts +84 -0
- package/src/i18n/lighting.he.ts +88 -0
- package/src/i18n/lighting.hi.ts +90 -0
- package/src/i18n/lighting.ja.ts +90 -0
- package/src/i18n/lighting.ko.ts +90 -0
- package/src/i18n/lighting.pt-BR.ts +90 -0
- package/src/i18n/lighting.ru.ts +88 -0
- package/src/i18n/lighting.zh-CN.ts +92 -0
- package/src/i18n/loop-subject.ar.ts +39 -0
- package/src/i18n/loop-subject.de.ts +39 -0
- package/src/i18n/loop-subject.es.ts +39 -0
- package/src/i18n/loop-subject.fr.ts +39 -0
- package/src/i18n/loop-subject.he.ts +39 -0
- package/src/i18n/loop-subject.hi.ts +39 -0
- package/src/i18n/loop-subject.ja.ts +39 -0
- package/src/i18n/loop-subject.ko.ts +39 -0
- package/src/i18n/loop-subject.pt-BR.ts +39 -0
- package/src/i18n/loop-subject.ru.ts +39 -0
- package/src/i18n/loop-subject.zh-CN.ts +39 -0
- package/src/i18n/materials.ar.ts +80 -0
- package/src/i18n/materials.de.ts +80 -0
- package/src/i18n/materials.es.ts +87 -0
- package/src/i18n/materials.fr.ts +80 -0
- package/src/i18n/materials.he.ts +86 -0
- package/src/i18n/materials.hi.ts +86 -0
- package/src/i18n/materials.ja.ts +88 -0
- package/src/i18n/materials.ko.ts +88 -0
- package/src/i18n/materials.pt-BR.ts +88 -0
- package/src/i18n/materials.ru.ts +86 -0
- package/src/i18n/materials.zh-CN.ts +86 -0
- package/src/i18n/mood.ar.ts +61 -0
- package/src/i18n/mood.de.ts +61 -0
- package/src/i18n/mood.es.ts +66 -0
- package/src/i18n/mood.fr.ts +61 -0
- package/src/i18n/mood.he.ts +64 -0
- package/src/i18n/mood.hi.ts +66 -0
- package/src/i18n/mood.ja.ts +66 -0
- package/src/i18n/mood.ko.ts +66 -0
- package/src/i18n/mood.pt-BR.ts +64 -0
- package/src/i18n/mood.ru.ts +64 -0
- package/src/i18n/mood.zh-CN.ts +64 -0
- package/src/i18n/music-genre.ar.ts +302 -0
- package/src/i18n/music-genre.de.ts +302 -0
- package/src/i18n/music-genre.es.ts +302 -0
- package/src/i18n/music-genre.fr.ts +302 -0
- package/src/i18n/music-genre.he.ts +302 -0
- package/src/i18n/music-genre.hi.ts +302 -0
- package/src/i18n/music-genre.ja.ts +302 -0
- package/src/i18n/music-genre.ko.ts +302 -0
- package/src/i18n/music-genre.pt-BR.ts +302 -0
- package/src/i18n/music-genre.ru.ts +302 -0
- package/src/i18n/music-genre.zh-CN.ts +302 -0
- package/src/i18n/music-mood.ar.ts +75 -0
- package/src/i18n/music-mood.de.ts +75 -0
- package/src/i18n/music-mood.es.ts +75 -0
- package/src/i18n/music-mood.fr.ts +75 -0
- package/src/i18n/music-mood.he.ts +75 -0
- package/src/i18n/music-mood.hi.ts +75 -0
- package/src/i18n/music-mood.ja.ts +75 -0
- package/src/i18n/music-mood.ko.ts +75 -0
- package/src/i18n/music-mood.pt-BR.ts +75 -0
- package/src/i18n/music-mood.ru.ts +75 -0
- package/src/i18n/music-mood.zh-CN.ts +75 -0
- package/src/i18n/person.ar.ts +638 -0
- package/src/i18n/person.de.ts +629 -0
- package/src/i18n/person.es.ts +645 -0
- package/src/i18n/person.fr.ts +621 -0
- package/src/i18n/person.he.ts +656 -0
- package/src/i18n/person.hi.ts +654 -0
- package/src/i18n/person.ja.ts +662 -0
- package/src/i18n/person.ko.ts +662 -0
- package/src/i18n/person.pt-BR.ts +656 -0
- package/src/i18n/person.ru.ts +656 -0
- package/src/i18n/person.zh-CN.ts +656 -0
- package/src/i18n/photo-genre.ar.ts +58 -0
- package/src/i18n/photo-genre.de.ts +52 -0
- package/src/i18n/photo-genre.es.ts +64 -0
- package/src/i18n/photo-genre.fr.ts +58 -0
- package/src/i18n/photo-genre.he.ts +62 -0
- package/src/i18n/photo-genre.hi.ts +63 -0
- package/src/i18n/photo-genre.ja.ts +65 -0
- package/src/i18n/photo-genre.ko.ts +63 -0
- package/src/i18n/photo-genre.pt-BR.ts +65 -0
- package/src/i18n/photo-genre.ru.ts +63 -0
- package/src/i18n/photo-genre.zh-CN.ts +63 -0
- package/src/i18n/photographer.ar.ts +75 -0
- package/src/i18n/photographer.de.ts +74 -0
- package/src/i18n/photographer.es.ts +77 -0
- package/src/i18n/photographer.fr.ts +77 -0
- package/src/i18n/photographer.he.ts +78 -0
- package/src/i18n/photographer.hi.ts +80 -0
- package/src/i18n/photographer.ja.ts +84 -0
- package/src/i18n/photographer.ko.ts +84 -0
- package/src/i18n/photographer.pt-BR.ts +83 -0
- package/src/i18n/photographer.ru.ts +78 -0
- package/src/i18n/photographer.zh-CN.ts +78 -0
- package/src/i18n/pose.ar.ts +95 -0
- package/src/i18n/pose.de.ts +96 -0
- package/src/i18n/pose.es.ts +104 -0
- package/src/i18n/pose.fr.ts +96 -0
- package/src/i18n/pose.he.ts +104 -0
- package/src/i18n/pose.hi.ts +104 -0
- package/src/i18n/pose.ja.ts +104 -0
- package/src/i18n/pose.ko.ts +104 -0
- package/src/i18n/pose.pt-BR.ts +104 -0
- package/src/i18n/pose.ru.ts +104 -0
- package/src/i18n/pose.zh-CN.ts +104 -0
- package/src/i18n/post-process-effects.ar.ts +25 -0
- package/src/i18n/post-process-effects.de.ts +24 -0
- package/src/i18n/post-process-effects.es.ts +26 -0
- package/src/i18n/post-process-effects.fr.ts +24 -0
- package/src/i18n/post-process-effects.he.ts +24 -0
- package/src/i18n/post-process-effects.hi.ts +26 -0
- package/src/i18n/post-process-effects.ja.ts +26 -0
- package/src/i18n/post-process-effects.ko.ts +26 -0
- package/src/i18n/post-process-effects.pt-BR.ts +24 -0
- package/src/i18n/post-process-effects.ru.ts +24 -0
- package/src/i18n/post-process-effects.zh-CN.ts +24 -0
- package/src/i18n/render-quality.ar.ts +34 -0
- package/src/i18n/render-quality.de.ts +30 -0
- package/src/i18n/render-quality.es.ts +35 -0
- package/src/i18n/render-quality.fr.ts +34 -0
- package/src/i18n/render-quality.he.ts +33 -0
- package/src/i18n/render-quality.hi.ts +39 -0
- package/src/i18n/render-quality.ja.ts +39 -0
- package/src/i18n/render-quality.ko.ts +39 -0
- package/src/i18n/render-quality.pt-BR.ts +37 -0
- package/src/i18n/render-quality.ru.ts +37 -0
- package/src/i18n/render-quality.zh-CN.ts +37 -0
- package/src/i18n/seasons.ar.ts +10 -0
- package/src/i18n/seasons.de.ts +10 -0
- package/src/i18n/seasons.es.ts +10 -0
- package/src/i18n/seasons.fr.ts +10 -0
- package/src/i18n/seasons.he.ts +10 -0
- package/src/i18n/seasons.hi.ts +10 -0
- package/src/i18n/seasons.ja.ts +10 -0
- package/src/i18n/seasons.ko.ts +10 -0
- package/src/i18n/seasons.pt-BR.ts +10 -0
- package/src/i18n/seasons.ru.ts +10 -0
- package/src/i18n/seasons.zh-CN.ts +10 -0
- package/src/i18n/setting.ar.ts +73 -0
- package/src/i18n/setting.de.ts +73 -0
- package/src/i18n/setting.es.ts +77 -0
- package/src/i18n/setting.fr.ts +73 -0
- package/src/i18n/setting.he.ts +76 -0
- package/src/i18n/setting.hi.ts +76 -0
- package/src/i18n/setting.ja.ts +78 -0
- package/src/i18n/setting.ko.ts +76 -0
- package/src/i18n/setting.pt-BR.ts +78 -0
- package/src/i18n/setting.ru.ts +76 -0
- package/src/i18n/setting.zh-CN.ts +76 -0
- package/src/i18n/style.ar.ts +55 -0
- package/src/i18n/style.de.ts +54 -0
- package/src/i18n/style.es.ts +54 -0
- package/src/i18n/style.fr.ts +54 -0
- package/src/i18n/style.he.ts +54 -0
- package/src/i18n/style.hi.ts +54 -0
- package/src/i18n/style.ja.ts +54 -0
- package/src/i18n/style.ko.ts +54 -0
- package/src/i18n/style.pt-BR.ts +56 -0
- package/src/i18n/style.ru.ts +54 -0
- package/src/i18n/style.zh-CN.ts +54 -0
- package/src/i18n/styling.ar.ts +295 -0
- package/src/i18n/styling.de.ts +285 -0
- package/src/i18n/styling.es.ts +317 -0
- package/src/i18n/styling.fr.ts +285 -0
- package/src/i18n/styling.he.ts +307 -0
- package/src/i18n/styling.hi.ts +323 -0
- package/src/i18n/styling.ja.ts +317 -0
- package/src/i18n/styling.ko.ts +311 -0
- package/src/i18n/styling.pt-BR.ts +301 -0
- package/src/i18n/styling.ru.ts +301 -0
- package/src/i18n/styling.zh-CN.ts +307 -0
- package/src/i18n/temporal.ar.ts +31 -0
- package/src/i18n/temporal.de.ts +24 -0
- package/src/i18n/temporal.es.ts +27 -0
- package/src/i18n/temporal.fr.ts +28 -0
- package/src/i18n/temporal.he.ts +31 -0
- package/src/i18n/temporal.hi.ts +31 -0
- package/src/i18n/temporal.ja.ts +31 -0
- package/src/i18n/temporal.ko.ts +31 -0
- package/src/i18n/temporal.pt-BR.ts +31 -0
- package/src/i18n/temporal.ru.ts +31 -0
- package/src/i18n/temporal.zh-CN.ts +31 -0
- package/src/i18n/transitions.ar.ts +97 -0
- package/src/i18n/transitions.de.ts +97 -0
- package/src/i18n/transitions.es.ts +97 -0
- package/src/i18n/transitions.fr.ts +97 -0
- package/src/i18n/transitions.he.ts +97 -0
- package/src/i18n/transitions.hi.ts +97 -0
- package/src/i18n/transitions.ja.ts +97 -0
- package/src/i18n/transitions.ko.ts +97 -0
- package/src/i18n/transitions.pt-BR.ts +97 -0
- package/src/i18n/transitions.ru.ts +97 -0
- package/src/i18n/transitions.zh-CN.ts +97 -0
- package/src/i18n/types.ts +158 -0
- package/src/i18n/vehicles.ar.ts +137 -0
- package/src/i18n/vehicles.de.ts +125 -0
- package/src/i18n/vehicles.es.ts +138 -0
- package/src/i18n/vehicles.fr.ts +125 -0
- package/src/i18n/vehicles.he.ts +136 -0
- package/src/i18n/vehicles.hi.ts +138 -0
- package/src/i18n/vehicles.ja.ts +138 -0
- package/src/i18n/vehicles.ko.ts +138 -0
- package/src/i18n/vehicles.pt-BR.ts +136 -0
- package/src/i18n/vehicles.ru.ts +136 -0
- package/src/i18n/vehicles.zh-CN.ts +136 -0
- package/src/i18n/voice-character.ar.ts +126 -0
- package/src/i18n/voice-character.de.ts +126 -0
- package/src/i18n/voice-character.es.ts +126 -0
- package/src/i18n/voice-character.fr.ts +126 -0
- package/src/i18n/voice-character.he.ts +126 -0
- package/src/i18n/voice-character.hi.ts +126 -0
- package/src/i18n/voice-character.ja.ts +126 -0
- package/src/i18n/voice-character.ko.ts +126 -0
- package/src/i18n/voice-character.pt-BR.ts +126 -0
- package/src/i18n/voice-character.ru.ts +126 -0
- package/src/i18n/voice-character.zh-CN.ts +126 -0
- package/src/i18n/voice-delivery.ar.ts +77 -0
- package/src/i18n/voice-delivery.de.ts +77 -0
- package/src/i18n/voice-delivery.es.ts +77 -0
- package/src/i18n/voice-delivery.fr.ts +77 -0
- package/src/i18n/voice-delivery.he.ts +77 -0
- package/src/i18n/voice-delivery.hi.ts +77 -0
- package/src/i18n/voice-delivery.ja.ts +77 -0
- package/src/i18n/voice-delivery.ko.ts +77 -0
- package/src/i18n/voice-delivery.pt-BR.ts +77 -0
- package/src/i18n/voice-delivery.ru.ts +77 -0
- package/src/i18n/voice-delivery.zh-CN.ts +77 -0
- package/src/i18n/weapons.ar.ts +115 -0
- package/src/i18n/weapons.de.ts +114 -0
- package/src/i18n/weapons.es.ts +116 -0
- package/src/i18n/weapons.fr.ts +103 -0
- package/src/i18n/weapons.he.ts +114 -0
- package/src/i18n/weapons.hi.ts +116 -0
- package/src/i18n/weapons.ja.ts +116 -0
- package/src/i18n/weapons.ko.ts +116 -0
- package/src/i18n/weapons.pt-BR.ts +114 -0
- package/src/i18n/weapons.ru.ts +114 -0
- package/src/i18n/weapons.zh-CN.ts +114 -0
- package/src/image-critic-node.ts +61 -0
- package/src/image-critic-types.ts +40 -0
- package/src/index.ts +866 -0
- package/src/inject-upstream.ts +15 -0
- package/src/inpaint-region.ts +51 -0
- package/src/json-evaluator.ts +732 -0
- package/src/json-path.ts +52 -0
- package/src/lip-sync-pricing.ts +90 -0
- package/src/list-merge.ts +40 -0
- package/src/list-sort.ts +122 -0
- package/src/list-source-resolver.ts +50 -0
- package/src/llm-models.ts +245 -0
- package/src/location-mention-slug.ts +291 -0
- package/src/location-preset-catalog-map.ts +128 -0
- package/src/loop-delimiter.ts +73 -0
- package/src/lora-routing.ts +95 -0
- package/src/lottie-overlay-catalog.ts +175 -0
- package/src/lottie-slots.ts +309 -0
- package/src/model-catalog.ts +2386 -0
- package/src/model-constants.ts +1910 -0
- package/src/model-tree.ts +120 -0
- package/src/monetization.ts +15 -0
- package/src/multi-pick.ts +42 -0
- package/src/node-default-mappings.ts +262 -0
- package/src/node-mappable-fields.ts +70 -0
- package/src/node-preset-export.ts +56 -0
- package/src/node-preset-extract.ts +147 -0
- package/src/node-refs.ts +253 -0
- package/src/node-runtime-keys.ts +117 -0
- package/src/object-aspect-defaults.ts +72 -0
- package/src/object-picker-types.ts +22 -0
- package/src/parameter-node-value.ts +284 -0
- package/src/pinterest-url.ts +28 -0
- package/src/pipeline-chat.ts +72 -0
- package/src/pipeline-defaults.ts +318 -0
- package/src/pipeline-events.ts +162 -0
- package/src/pipeline-state-types.ts +196 -0
- package/src/pipeline-types.ts +647 -0
- package/src/pipeline-validation.ts +87 -0
- package/src/presentation-types.ts +23 -0
- package/src/presentation-utils.ts +397 -0
- package/src/producer-types.ts +188 -0
- package/src/progress-curve.ts +99 -0
- package/src/provider-directive-defaults.ts +69 -0
- package/src/reduce-strategy-registry.ts +121 -0
- package/src/reference-board-templates.ts +81 -0
- package/src/reference-roles.ts +153 -0
- package/src/reference-sheet/__tests__/board-to-column.test.ts +20 -0
- package/src/reference-sheet/__tests__/catalog.test.ts +50 -0
- package/src/reference-sheet/__tests__/cost.test.ts +38 -0
- package/src/reference-sheet/__tests__/no-drift.test.ts +28 -0
- package/src/reference-sheet/__tests__/panel-plan.test.ts +61 -0
- package/src/reference-sheet/__tests__/panel-prompts.test.ts +17 -0
- package/src/reference-sheet/__tests__/plan-generation.test.ts +72 -0
- package/src/reference-sheet/__tests__/presets.test.ts +54 -0
- package/src/reference-sheet/__tests__/types.test.ts +24 -0
- package/src/reference-sheet/catalog.ts +144 -0
- package/src/reference-sheet/cost.ts +37 -0
- package/src/reference-sheet/credit-id.ts +7 -0
- package/src/reference-sheet/index.ts +8 -0
- package/src/reference-sheet/panel-plan.ts +65 -0
- package/src/reference-sheet/panel-prompts.ts +14 -0
- package/src/reference-sheet/plan-generation.ts +79 -0
- package/src/reference-sheet/presets.ts +77 -0
- package/src/reference-sheet/types.ts +92 -0
- package/src/repeat-types.ts +97 -0
- package/src/resolve-field-mappings.ts +119 -0
- package/src/route-filter.ts +80 -0
- package/src/scene-helper-types.ts +249 -0
- package/src/scene-node-types.ts +281 -0
- package/src/scraper-actors.ts +57 -0
- package/src/scraper-output-schemas.ts +25 -0
- package/src/seasons.ts +45 -0
- package/src/selector.ts +864 -0
- package/src/settled-with-limit.ts +42 -0
- package/src/shot-element.ts +43 -0
- package/src/social-media-specs.ts +121 -0
- package/src/social-post.ts +24 -0
- package/src/sub-workflow-validation.ts +78 -0
- package/src/supported-fonts.ts +40 -0
- package/src/surround.ts +113 -0
- package/src/switchx-pricing.ts +33 -0
- package/src/text-separators.ts +59 -0
- package/src/to-connected-references.ts +77 -0
- package/src/types.ts +272 -0
- package/src/variant-job-id.ts +17 -0
- package/src/vehicles.ts +213 -0
- package/src/video-analysis-pricing.ts +74 -0
- package/src/video-analysis.ts +127 -0
- package/src/video-duration.ts +25 -0
- package/src/voice-changer-models.ts +36 -0
- package/src/voice-matcher-types.ts +37 -0
- package/src/voices.ts +92 -0
- package/src/weapons.ts +190 -0
- package/src/workflow-export.ts +153 -0
package/src/selector.ts
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge range utilities for resolving 1-based index expressions
|
|
3
|
+
* and applying range/step slicing to output lists.
|
|
4
|
+
*
|
|
5
|
+
* Used by both frontend DAG executor and backend orchestrator
|
|
6
|
+
* to support edge-level range, step, and item selection.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { evaluateCondition, substituteTriggerTokens, tryParseJson } from "./filter-condition.js"
|
|
10
|
+
import { resolveNodeRefs } from "./node-refs.js"
|
|
11
|
+
|
|
12
|
+
/** Selector tab on an edge — "range" (from/to/step) or "list" (expression). */
|
|
13
|
+
export type SelectorMode = "range" | "list"
|
|
14
|
+
|
|
15
|
+
/** Per-edge output mode controlling how upstream results reach downstream. */
|
|
16
|
+
export type OutputMode = "last" | "each" | "all" | "item"
|
|
17
|
+
|
|
18
|
+
/** Edge-data fields consumed by selectListItems / isDefaultSelectorConfig. */
|
|
19
|
+
export type SelectorFields = {
|
|
20
|
+
selectorMode?: SelectorMode
|
|
21
|
+
listExpression?: string
|
|
22
|
+
rangeFrom?: string
|
|
23
|
+
rangeTo?: string
|
|
24
|
+
rangeStep?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Full selector mode set — extends the edge-selector modes with new operators. */
|
|
28
|
+
export type FullSelectorMode =
|
|
29
|
+
| "item"
|
|
30
|
+
| "range"
|
|
31
|
+
| "list"
|
|
32
|
+
| "random"
|
|
33
|
+
| "modulo"
|
|
34
|
+
| "predicate"
|
|
35
|
+
| "named-key"
|
|
36
|
+
|
|
37
|
+
/** Operator set for predicate mode — reused from filter-list. */
|
|
38
|
+
export type SelectorPredicateOp =
|
|
39
|
+
| ">" | "<" | ">=" | "<="
|
|
40
|
+
| "=" | "!="
|
|
41
|
+
| "contains" | "not_contains"
|
|
42
|
+
| "starts_with" | "ends_with"
|
|
43
|
+
| "regex"
|
|
44
|
+
| "exists" | "not_exists"
|
|
45
|
+
|
|
46
|
+
/** Config consumed by `runSelector` and `resolveSelectorRefs`. */
|
|
47
|
+
export type SelectorConfig = {
|
|
48
|
+
mode: FullSelectorMode
|
|
49
|
+
// item
|
|
50
|
+
itemIndex?: string
|
|
51
|
+
// range
|
|
52
|
+
rangeFrom?: string
|
|
53
|
+
rangeTo?: string
|
|
54
|
+
rangeStep?: number
|
|
55
|
+
// list
|
|
56
|
+
listExpression?: string
|
|
57
|
+
// random
|
|
58
|
+
seed?: string
|
|
59
|
+
randomCount?: number
|
|
60
|
+
// modulo
|
|
61
|
+
moduloDivisor?: string
|
|
62
|
+
// predicate
|
|
63
|
+
predicateField?: string
|
|
64
|
+
predicateOp?: SelectorPredicateOp
|
|
65
|
+
predicateValue?: string
|
|
66
|
+
predicateMatch?: "first" | "all"
|
|
67
|
+
predicateCaseSensitive?: boolean
|
|
68
|
+
// named-key
|
|
69
|
+
namedKeyField?: string
|
|
70
|
+
namedKeyValue?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Result of a selector run. `picked ∪ rest = items`, `picked ∩ rest = ∅`. */
|
|
74
|
+
export type SelectorResult = { picked: string[]; rest: string[] }
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolves a 1-based index expression to a 0-based array index.
|
|
78
|
+
*
|
|
79
|
+
* Supported expressions:
|
|
80
|
+
* - "1", "2", ... — absolute 1-based index
|
|
81
|
+
* - "last" — last element
|
|
82
|
+
* - "last-1", "last-2", ... — relative from end
|
|
83
|
+
*
|
|
84
|
+
* Out-of-bounds values are clamped. Malformed input falls back to `defaultExpr`.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveIndex(
|
|
87
|
+
expr: string,
|
|
88
|
+
listLength: number,
|
|
89
|
+
defaultExpr: string = "1",
|
|
90
|
+
): number {
|
|
91
|
+
if (listLength <= 0) return 0
|
|
92
|
+
|
|
93
|
+
const trimmed = expr.trim()
|
|
94
|
+
let index: number
|
|
95
|
+
|
|
96
|
+
if (trimmed === "last") {
|
|
97
|
+
index = listLength - 1
|
|
98
|
+
} else if (trimmed.startsWith("last-")) {
|
|
99
|
+
const offset = parseInt(trimmed.slice(5), 10)
|
|
100
|
+
if (isNaN(offset) || offset < 0) return resolveIndex(defaultExpr === expr ? "1" : defaultExpr, listLength)
|
|
101
|
+
index = listLength - 1 - offset
|
|
102
|
+
} else {
|
|
103
|
+
const n = parseInt(trimmed, 10)
|
|
104
|
+
if (isNaN(n)) return resolveIndex(defaultExpr === expr ? "1" : defaultExpr, listLength)
|
|
105
|
+
index = n - 1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return Math.max(0, Math.min(index, listLength - 1))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Applies range and step slicing to a list of strings.
|
|
113
|
+
*
|
|
114
|
+
* `from` and `to` are 1-based index expressions (see `resolveIndex`).
|
|
115
|
+
* `step` controls iteration direction and stride:
|
|
116
|
+
* - positive step: forward iteration (from must be <= to)
|
|
117
|
+
* - negative step: reverse iteration (from must be >= to)
|
|
118
|
+
* - step 0 is treated as step 1
|
|
119
|
+
*
|
|
120
|
+
* Returns empty array if direction mismatches (e.g., from < to with negative step).
|
|
121
|
+
*/
|
|
122
|
+
export function applyRange(
|
|
123
|
+
list: string[],
|
|
124
|
+
from?: string,
|
|
125
|
+
to?: string,
|
|
126
|
+
step?: number,
|
|
127
|
+
): string[] {
|
|
128
|
+
if (list.length === 0) return []
|
|
129
|
+
|
|
130
|
+
const fromIdx = resolveIndex(from ?? "1", list.length, "1")
|
|
131
|
+
const toIdx = resolveIndex(to ?? "last", list.length, "last")
|
|
132
|
+
const effectiveStep = step === 0 || step === undefined ? 1 : step
|
|
133
|
+
|
|
134
|
+
if (effectiveStep > 0 && fromIdx > toIdx) return []
|
|
135
|
+
if (effectiveStep < 0 && fromIdx < toIdx) return []
|
|
136
|
+
|
|
137
|
+
const result: string[] = []
|
|
138
|
+
if (effectiveStep > 0) {
|
|
139
|
+
for (let i = fromIdx; i <= toIdx; i += effectiveStep) result.push(list[i])
|
|
140
|
+
} else {
|
|
141
|
+
for (let i = fromIdx; i >= toIdx; i += effectiveStep) result.push(list[i])
|
|
142
|
+
}
|
|
143
|
+
return result
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Migrates legacy `item:N` (0-based) outputMode to structured format.
|
|
148
|
+
*
|
|
149
|
+
* Legacy: `{ outputMode: "item:0" }` — 0-based index baked into mode string
|
|
150
|
+
* New: `{ outputMode: "item", itemIndex: "1" }` — 1-based expression
|
|
151
|
+
*
|
|
152
|
+
* Returns the data unchanged if not a legacy item mode.
|
|
153
|
+
*/
|
|
154
|
+
export function migrateEdgeOutputMode(
|
|
155
|
+
data: Record<string, unknown> | undefined,
|
|
156
|
+
): Record<string, unknown> | undefined {
|
|
157
|
+
if (!data) return data
|
|
158
|
+
|
|
159
|
+
const mode = data.outputMode as string | undefined
|
|
160
|
+
if (!mode || !mode.startsWith("item:")) return data
|
|
161
|
+
|
|
162
|
+
const idx = parseInt(mode.split(":")[1], 10)
|
|
163
|
+
return {
|
|
164
|
+
...data,
|
|
165
|
+
outputMode: "item",
|
|
166
|
+
itemIndex: String(isNaN(idx) ? 1 : idx + 1),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Builds the edge range label shown as a pill on the edge.
|
|
172
|
+
* Examples:
|
|
173
|
+
* - "2..last-1 +2" — range with step 2
|
|
174
|
+
* - "last..1 -1" — reversed
|
|
175
|
+
* - "3" — item mode, index 3
|
|
176
|
+
* - undefined — "last" mode has no label
|
|
177
|
+
*/
|
|
178
|
+
export function buildRangeLabel(
|
|
179
|
+
mode: string,
|
|
180
|
+
rangeFrom?: string,
|
|
181
|
+
rangeTo?: string,
|
|
182
|
+
rangeStep?: number,
|
|
183
|
+
itemIndex?: string,
|
|
184
|
+
selectorMode?: SelectorMode,
|
|
185
|
+
listExpression?: string,
|
|
186
|
+
): string | undefined {
|
|
187
|
+
return buildItemLabel(mode, rangeFrom, rangeTo, rangeStep, itemIndex, selectorMode, listExpression)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Internal: builds the item-side label (range / list / item / each). Mode "last" returns undefined. */
|
|
191
|
+
function buildItemLabel(
|
|
192
|
+
mode: string,
|
|
193
|
+
rangeFrom?: string,
|
|
194
|
+
rangeTo?: string,
|
|
195
|
+
rangeStep?: number,
|
|
196
|
+
itemIndex?: string,
|
|
197
|
+
selectorMode?: SelectorMode,
|
|
198
|
+
listExpression?: string,
|
|
199
|
+
): string | undefined {
|
|
200
|
+
if (mode === "last") return undefined
|
|
201
|
+
|
|
202
|
+
if ((mode === "each" || mode === "all") && selectorMode === "list") {
|
|
203
|
+
const trimmed = (listExpression ?? "").trim()
|
|
204
|
+
if (trimmed === "") return undefined
|
|
205
|
+
return truncateLabel(trimmed, 18)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (mode === "item") return itemIndex || undefined
|
|
209
|
+
|
|
210
|
+
const from = rangeFrom ?? "1"
|
|
211
|
+
const to = rangeTo ?? "last"
|
|
212
|
+
const step = rangeStep ?? 1
|
|
213
|
+
const isDefaultRange = from === "1" && to === "last"
|
|
214
|
+
const isDefaultStep = step === 1
|
|
215
|
+
if (isDefaultRange && isDefaultStep) return undefined
|
|
216
|
+
|
|
217
|
+
let label = `${from}..${to}`
|
|
218
|
+
if ((mode === "each" || mode === "all") && !isDefaultStep) {
|
|
219
|
+
label += step > 0 ? ` +${step}` : ` ${step}`
|
|
220
|
+
}
|
|
221
|
+
return label
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function truncateLabel(s: string, maxLen: number): string {
|
|
225
|
+
if (s.length <= maxLen) return s
|
|
226
|
+
return s.slice(0, maxLen) + "…"
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Validates a list expression without resolving indices.
|
|
231
|
+
* Used for live UI validation in the List tab input.
|
|
232
|
+
*
|
|
233
|
+
* Returns { ok: true } for empty/whitespace input — matches the
|
|
234
|
+
* "empty = all items" runtime semantics.
|
|
235
|
+
*/
|
|
236
|
+
export function parseListExpression(
|
|
237
|
+
expr: string,
|
|
238
|
+
): { ok: true } | { ok: false; error: string } {
|
|
239
|
+
const result = parseListTerms(expr.trim())
|
|
240
|
+
return result.ok ? { ok: true } : { ok: false, error: result.error }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Internal: validates a single index token ("1", "last", "last-3"). */
|
|
244
|
+
function isValidIndexToken(token: string): boolean {
|
|
245
|
+
if (token === "last") return true
|
|
246
|
+
if (/^[1-9]\d*$/.test(token)) return true
|
|
247
|
+
const m = /^last-(\d+)$/.exec(token)
|
|
248
|
+
if (m) return !isNaN(parseInt(m[1], 10))
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Parses a list expression and returns 0-based indices into the list,
|
|
254
|
+
* in user-specified order. Preserves duplicates.
|
|
255
|
+
*
|
|
256
|
+
* Supported syntax:
|
|
257
|
+
* <expr> := <term> ("," <term>)*
|
|
258
|
+
* <term> := <index> | <index> ".." <index> (":" <step>)?
|
|
259
|
+
* <index> := <number> | "last" | "last-" <number>
|
|
260
|
+
* <step> := integer (0 coerced to 1)
|
|
261
|
+
*
|
|
262
|
+
* Clamps out-of-bounds indices. Malformed input logs a warning and
|
|
263
|
+
* returns [0..listLength-1] (matches applyRange leniency).
|
|
264
|
+
*/
|
|
265
|
+
export function resolveListExpression(expr: string, listLength: number): number[] {
|
|
266
|
+
if (listLength <= 0) return []
|
|
267
|
+
const result = parseListTerms(expr.trim())
|
|
268
|
+
if (!result.ok) {
|
|
269
|
+
console.warn("[edge-range] invalid list expression:", expr, "—", result.error)
|
|
270
|
+
return indexRange(0, listLength - 1, 1)
|
|
271
|
+
}
|
|
272
|
+
if (result.terms.length === 0) return indexRange(0, listLength - 1, 1)
|
|
273
|
+
|
|
274
|
+
const out: number[] = []
|
|
275
|
+
for (const term of result.terms) {
|
|
276
|
+
if (term.kind === "index") {
|
|
277
|
+
out.push(resolveIndex(term.token, listLength))
|
|
278
|
+
} else {
|
|
279
|
+
const from = resolveIndex(term.from, listLength)
|
|
280
|
+
const to = resolveIndex(term.to, listLength)
|
|
281
|
+
for (const idx of indexRange(from, to, term.step)) out.push(idx)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return out
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Internal: inclusive index range with step. Returns [] if direction mismatches. */
|
|
288
|
+
function indexRange(from: number, to: number, step: number): number[] {
|
|
289
|
+
const out: number[] = []
|
|
290
|
+
if (step > 0) {
|
|
291
|
+
if (from > to) return []
|
|
292
|
+
for (let i = from; i <= to; i += step) out.push(i)
|
|
293
|
+
} else {
|
|
294
|
+
if (from < to) return []
|
|
295
|
+
for (let i = from; i >= to; i += step) out.push(i)
|
|
296
|
+
}
|
|
297
|
+
return out
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Dispatches to either applyRange() or resolveListExpression() based on
|
|
302
|
+
* edgeData.selectorMode. Returns a new string[] containing the selected
|
|
303
|
+
* subset of items — or `items` unchanged when the edge has no filter.
|
|
304
|
+
*/
|
|
305
|
+
export function selectListItems(
|
|
306
|
+
items: string[],
|
|
307
|
+
edgeData: SelectorFields | undefined,
|
|
308
|
+
): string[] {
|
|
309
|
+
if (items.length === 0) return []
|
|
310
|
+
if (isDefaultSelectorConfig(edgeData)) return items
|
|
311
|
+
if (edgeData?.selectorMode === "list") {
|
|
312
|
+
const indices = resolveListExpression(edgeData.listExpression ?? "", items.length)
|
|
313
|
+
return indices.map((i) => items[i])
|
|
314
|
+
}
|
|
315
|
+
return applyRange(items, edgeData?.rangeFrom, edgeData?.rangeTo, edgeData?.rangeStep)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Returns true when edge data carries no active filter — empty/whitespace list
|
|
320
|
+
* expression in list mode, or defaults (1..last, step 1) in range mode.
|
|
321
|
+
* Callers can skip filtering entirely when this returns true.
|
|
322
|
+
*/
|
|
323
|
+
export function isDefaultSelectorConfig(edgeData: SelectorFields | undefined): boolean {
|
|
324
|
+
if (!edgeData) return true
|
|
325
|
+
if (edgeData.selectorMode === "list") {
|
|
326
|
+
return ((edgeData.listExpression ?? "").trim()) === ""
|
|
327
|
+
}
|
|
328
|
+
const { from, to, step } = canonicalRange(edgeData)
|
|
329
|
+
return from === "1" && to === "last" && step === 1
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function describeEdgeBehavior(
|
|
333
|
+
edgeData:
|
|
334
|
+
| {
|
|
335
|
+
outputMode?: string
|
|
336
|
+
selectorMode?: SelectorMode
|
|
337
|
+
listExpression?: string
|
|
338
|
+
rangeFrom?: string
|
|
339
|
+
rangeTo?: string
|
|
340
|
+
rangeStep?: number
|
|
341
|
+
itemIndex?: string
|
|
342
|
+
}
|
|
343
|
+
| undefined,
|
|
344
|
+
): string {
|
|
345
|
+
const mode = edgeData?.outputMode
|
|
346
|
+
return buildBaseSentence(edgeData, mode)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildBaseSentence(
|
|
350
|
+
edgeData: Parameters<typeof describeEdgeBehavior>[0],
|
|
351
|
+
mode: string | undefined,
|
|
352
|
+
): string {
|
|
353
|
+
if (mode === "last") return "Passes the selected result."
|
|
354
|
+
if (mode === "item") return buildItemSentence(edgeData)
|
|
355
|
+
if (mode === "each") return buildEachSentence(edgeData)
|
|
356
|
+
if (mode === "all") return buildAllSentence(edgeData)
|
|
357
|
+
return "Passes all items at once."
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function buildItemSentence(
|
|
361
|
+
edgeData: Parameters<typeof describeEdgeBehavior>[0],
|
|
362
|
+
): string {
|
|
363
|
+
const canonical = canonicalItemIndex(edgeData?.itemIndex)
|
|
364
|
+
return `Passes only ${itemModeForm(canonical)}.`
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function buildEachSentence(
|
|
368
|
+
edgeData: Parameters<typeof describeEdgeBehavior>[0],
|
|
369
|
+
): string {
|
|
370
|
+
if (isDefaultSelectorConfig(edgeData)) return "Runs the next node one by one, per item."
|
|
371
|
+
if (edgeData?.selectorMode === "list") {
|
|
372
|
+
const result = parseListTerms((edgeData.listExpression ?? "").trim())
|
|
373
|
+
if (!result.ok) return "Runs the next node one by one, per item."
|
|
374
|
+
}
|
|
375
|
+
const phrase = buildSelectionPhrase(edgeData)
|
|
376
|
+
if (phrase.kind === "empty-result") {
|
|
377
|
+
return "Nothing selected — the next node won't run."
|
|
378
|
+
}
|
|
379
|
+
if (phrase.kind === "single-index") {
|
|
380
|
+
return `Runs the next node only on ${phrase.itemModeForm}.`
|
|
381
|
+
}
|
|
382
|
+
return `Runs the next node once for ${phrase.text}.`
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function buildAllSentence(
|
|
386
|
+
edgeData: Parameters<typeof describeEdgeBehavior>[0],
|
|
387
|
+
): string {
|
|
388
|
+
if (isDefaultSelectorConfig(edgeData)) return "Passes all items at once."
|
|
389
|
+
if (edgeData?.selectorMode === "list") {
|
|
390
|
+
const result = parseListTerms((edgeData.listExpression ?? "").trim())
|
|
391
|
+
if (!result.ok) return "Passes all items at once."
|
|
392
|
+
}
|
|
393
|
+
const phrase = buildSelectionPhrase(edgeData)
|
|
394
|
+
if (phrase.kind === "empty-result") {
|
|
395
|
+
return "Nothing selected — the next node will get an empty bundle."
|
|
396
|
+
}
|
|
397
|
+
if (phrase.kind === "single-index") {
|
|
398
|
+
return `Passes only ${phrase.itemModeForm}.`
|
|
399
|
+
}
|
|
400
|
+
return `Passes ${phrase.text} at once.`
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
type CanonicalRange = { from: string; to: string; step: number }
|
|
404
|
+
|
|
405
|
+
function canonicalItemIndex(raw: string | undefined): string {
|
|
406
|
+
const trimmed = (raw ?? "").trim()
|
|
407
|
+
if (trimmed === "") return "1"
|
|
408
|
+
if (!isValidIndexToken(trimmed)) return "1"
|
|
409
|
+
return trimmed
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function canonicalRange(
|
|
413
|
+
edgeData: Parameters<typeof describeEdgeBehavior>[0],
|
|
414
|
+
): CanonicalRange {
|
|
415
|
+
const from = ((edgeData?.rangeFrom ?? "").trim() || "1")
|
|
416
|
+
const to = ((edgeData?.rangeTo ?? "").trim() || "last")
|
|
417
|
+
const rawStep = edgeData?.rangeStep
|
|
418
|
+
const step = rawStep === undefined || rawStep === 0 ? 1 : rawStep
|
|
419
|
+
return { from, to, step }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
function itemModeForm(canonical: string): string {
|
|
424
|
+
if (canonical === "1") return "the first item"
|
|
425
|
+
if (/^\d+$/.test(canonical)) return `item ${canonical}`
|
|
426
|
+
if (canonical === "last" || canonical === "last-0") return "the last item"
|
|
427
|
+
const m = /^last-(\d+)$/.exec(canonical)
|
|
428
|
+
if (m) {
|
|
429
|
+
const n = parseInt(m[1], 10)
|
|
430
|
+
if (n === 1) return "the second-to-last item"
|
|
431
|
+
if (n === 2) return "the third-to-last item"
|
|
432
|
+
if (n === 3) return "the fourth-to-last item"
|
|
433
|
+
return `the ${ordinal(n + 1)}-from-last item`
|
|
434
|
+
}
|
|
435
|
+
return "the first item"
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function compactForm(canonical: string): string {
|
|
439
|
+
if (/^\d+$/.test(canonical)) return canonical
|
|
440
|
+
if (canonical === "last" || canonical === "last-0") return "the last one"
|
|
441
|
+
const m = /^last-(\d+)$/.exec(canonical)
|
|
442
|
+
if (m) {
|
|
443
|
+
const n = parseInt(m[1], 10)
|
|
444
|
+
if (n === 1) return "the second-to-last"
|
|
445
|
+
if (n === 2) return "the third-to-last"
|
|
446
|
+
if (n === 3) return "the fourth-to-last"
|
|
447
|
+
return `the ${ordinal(n + 1)}-from-last`
|
|
448
|
+
}
|
|
449
|
+
return canonical
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function ordinal(n: number): string {
|
|
453
|
+
const absN = Math.abs(n)
|
|
454
|
+
const lastTwo = absN % 100
|
|
455
|
+
if (lastTwo >= 11 && lastTwo <= 13) return `${n}th`
|
|
456
|
+
switch (absN % 10) {
|
|
457
|
+
case 1: return `${n}st`
|
|
458
|
+
case 2: return `${n}nd`
|
|
459
|
+
case 3: return `${n}rd`
|
|
460
|
+
default: return `${n}th`
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
type SelectionPhrase =
|
|
465
|
+
| { kind: "items"; text: string }
|
|
466
|
+
| { kind: "single-index"; itemModeForm: string }
|
|
467
|
+
| { kind: "empty-result" }
|
|
468
|
+
|
|
469
|
+
function buildSelectionPhrase(
|
|
470
|
+
edgeData: Parameters<typeof describeEdgeBehavior>[0],
|
|
471
|
+
): SelectionPhrase {
|
|
472
|
+
if (edgeData?.selectorMode === "list") {
|
|
473
|
+
return buildListSelectionPhrase((edgeData.listExpression ?? "").trim())
|
|
474
|
+
}
|
|
475
|
+
return buildRangeSelectionPhrase(canonicalRange(edgeData))
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function indexKind(token: string): "concrete" | "relative" | "invalid" {
|
|
479
|
+
if (/^[1-9]\d*$/.test(token)) return "concrete"
|
|
480
|
+
if (token === "last" || /^last-\d+$/.test(token)) return "relative"
|
|
481
|
+
return "invalid"
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function relativeOffset(token: string): number {
|
|
485
|
+
if (token === "last") return 0
|
|
486
|
+
const m = /^last-(\d+)$/.exec(token)
|
|
487
|
+
return m ? parseInt(m[1], 10) : 0
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function compareTokens(a: string, b: string): -1 | 0 | 1 | "indeterminate" {
|
|
491
|
+
const ka = indexKind(a)
|
|
492
|
+
const kb = indexKind(b)
|
|
493
|
+
if (ka !== kb) return "indeterminate"
|
|
494
|
+
if (ka === "concrete") {
|
|
495
|
+
const na = parseInt(a, 10)
|
|
496
|
+
const nb = parseInt(b, 10)
|
|
497
|
+
return na < nb ? -1 : na > nb ? 1 : 0
|
|
498
|
+
}
|
|
499
|
+
if (ka === "relative") {
|
|
500
|
+
const oa = relativeOffset(a)
|
|
501
|
+
const ob = relativeOffset(b)
|
|
502
|
+
return oa < ob ? 1 : oa > ob ? -1 : 0
|
|
503
|
+
}
|
|
504
|
+
return "indeterminate"
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function buildRangeSelectionPhrase(r: CanonicalRange): SelectionPhrase {
|
|
508
|
+
const { from, to, step } = r
|
|
509
|
+
|
|
510
|
+
const cmp = compareTokens(from, to)
|
|
511
|
+
if (cmp !== "indeterminate") {
|
|
512
|
+
if (step > 0 && cmp === 1) return { kind: "empty-result" }
|
|
513
|
+
if (step < 0 && cmp === -1) return { kind: "empty-result" }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (cmp === 0) {
|
|
517
|
+
return { kind: "single-index", itemModeForm: itemModeForm(from) }
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const absStep = Math.abs(step)
|
|
521
|
+
if (step === -1 && from === "last" && to === "1") {
|
|
522
|
+
return { kind: "items", text: "all items in reverse order" }
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
let text = `items ${compactForm(from)} through ${compactForm(to)}`
|
|
526
|
+
if (absStep === 1 && step < 0) {
|
|
527
|
+
text += " in reverse order"
|
|
528
|
+
} else if (absStep > 1 && step > 0) {
|
|
529
|
+
text += ` (every ${ordinal(absStep)} item)`
|
|
530
|
+
} else if (absStep > 1 && step < 0) {
|
|
531
|
+
text += ` (every ${ordinal(absStep)} item, in reverse)`
|
|
532
|
+
}
|
|
533
|
+
return { kind: "items", text }
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
type ListTerm =
|
|
537
|
+
| { kind: "index"; token: string }
|
|
538
|
+
| { kind: "range"; from: string; to: string; step: number }
|
|
539
|
+
|
|
540
|
+
type ParseResult =
|
|
541
|
+
| { ok: true; terms: ListTerm[] }
|
|
542
|
+
| { ok: false; error: string }
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Core list-expression parser. Returns structured terms on success or a
|
|
546
|
+
* specific error message on failure. Empty input → zero terms (not an error).
|
|
547
|
+
* Error messages are surfaced unchanged in the edge-config tooltip, so they
|
|
548
|
+
* must stay stable:
|
|
549
|
+
* - "Empty item between commas"
|
|
550
|
+
* - "Invalid index: <token>"
|
|
551
|
+
* - "Range missing endpoint: <term>"
|
|
552
|
+
* - "Step must be an integer"
|
|
553
|
+
*/
|
|
554
|
+
function parseListTerms(expr: string): ParseResult {
|
|
555
|
+
if (expr === "") return { ok: true, terms: [] }
|
|
556
|
+
const terms: ListTerm[] = []
|
|
557
|
+
const raw = expr.split(",")
|
|
558
|
+
for (const rawTerm of raw) {
|
|
559
|
+
const term = rawTerm.trim()
|
|
560
|
+
if (term === "") return { ok: false, error: "Empty item between commas" }
|
|
561
|
+
const rangeIdx = term.indexOf("..")
|
|
562
|
+
if (rangeIdx === -1) {
|
|
563
|
+
if (!isValidIndexToken(term)) return { ok: false, error: `Invalid index: ${term}` }
|
|
564
|
+
terms.push({ kind: "index", token: term })
|
|
565
|
+
continue
|
|
566
|
+
}
|
|
567
|
+
const left = term.slice(0, rangeIdx).trim()
|
|
568
|
+
const rightWithStep = term.slice(rangeIdx + 2).trim()
|
|
569
|
+
if (left === "" || rightWithStep === "") {
|
|
570
|
+
return { ok: false, error: `Range missing endpoint: ${term}` }
|
|
571
|
+
}
|
|
572
|
+
const colonIdx = rightWithStep.indexOf(":")
|
|
573
|
+
const right = colonIdx === -1 ? rightWithStep : rightWithStep.slice(0, colonIdx).trim()
|
|
574
|
+
const stepStr = colonIdx === -1 ? null : rightWithStep.slice(colonIdx + 1).trim()
|
|
575
|
+
if (right === "") return { ok: false, error: `Range missing endpoint: ${term}` }
|
|
576
|
+
if (!isValidIndexToken(left)) return { ok: false, error: `Invalid index: ${left}` }
|
|
577
|
+
if (!isValidIndexToken(right)) return { ok: false, error: `Invalid index: ${right}` }
|
|
578
|
+
let step = 1
|
|
579
|
+
if (stepStr !== null) {
|
|
580
|
+
if (!/^-?\d+$/.test(stepStr)) return { ok: false, error: "Step must be an integer" }
|
|
581
|
+
const parsed = parseInt(stepStr, 10)
|
|
582
|
+
step = parsed === 0 ? 1 : parsed
|
|
583
|
+
}
|
|
584
|
+
terms.push({ kind: "range", from: left, to: right, step })
|
|
585
|
+
}
|
|
586
|
+
return { ok: true, terms }
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function buildListSelectionPhrase(expr: string): SelectionPhrase {
|
|
590
|
+
const result = parseListTerms(expr)
|
|
591
|
+
if (!result.ok) return { kind: "empty-result" }
|
|
592
|
+
if (result.terms.length === 0) return { kind: "items", text: "items" } // unreachable via callers; default-config catches empty
|
|
593
|
+
|
|
594
|
+
const normalized = result.terms.map<ListTerm>((t) => {
|
|
595
|
+
if (t.kind === "range") {
|
|
596
|
+
const cmp = compareTokens(t.from, t.to)
|
|
597
|
+
if (cmp === 0) return { kind: "index", token: t.from }
|
|
598
|
+
}
|
|
599
|
+
return t
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
if (normalized.length === 1 && normalized[0].kind === "index") {
|
|
603
|
+
return { kind: "single-index", itemModeForm: itemModeForm(normalized[0].token) }
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const rendered = normalized.map((t) => {
|
|
607
|
+
if (t.kind === "index") return compactForm(t.token)
|
|
608
|
+
let text = `${compactForm(t.from)} through ${compactForm(t.to)}`
|
|
609
|
+
const absStep = Math.abs(t.step)
|
|
610
|
+
if (absStep === 1 && t.step < 0) {
|
|
611
|
+
if (t.from === "last" && t.to === "1") return "all items in reverse"
|
|
612
|
+
text += " in reverse"
|
|
613
|
+
} else if (absStep > 1 && t.step > 0) {
|
|
614
|
+
text += ` (every ${ordinal(absStep)})`
|
|
615
|
+
} else if (absStep > 1 && t.step < 0) {
|
|
616
|
+
text += ` (every ${ordinal(absStep)}, in reverse)`
|
|
617
|
+
}
|
|
618
|
+
return text
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
let joined: string
|
|
622
|
+
if (rendered.length === 1) {
|
|
623
|
+
joined = rendered[0]
|
|
624
|
+
} else if (rendered.length === 2) {
|
|
625
|
+
joined = `${rendered[0]} and ${rendered[1]}`
|
|
626
|
+
} else {
|
|
627
|
+
const head = rendered.slice(0, -1).join(", ")
|
|
628
|
+
joined = `${head}, and ${rendered[rendered.length - 1]}`
|
|
629
|
+
}
|
|
630
|
+
return { kind: "items", text: `items ${joined}` }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Run the selector against `items`. Returns the picked subset plus the
|
|
635
|
+
* complement (`rest`). For modes that pick by index, `rest` is computed by
|
|
636
|
+
* index exclusion (not value) so duplicate items are handled correctly.
|
|
637
|
+
*/
|
|
638
|
+
export function runSelector(items: string[], config: SelectorConfig): SelectorResult {
|
|
639
|
+
if (items.length === 0) return { picked: [], rest: [] }
|
|
640
|
+
|
|
641
|
+
switch (config.mode) {
|
|
642
|
+
case "item": {
|
|
643
|
+
const idx = resolveIndex(config.itemIndex ?? "1", items.length, "1")
|
|
644
|
+
return splitByIndices(items, [idx])
|
|
645
|
+
}
|
|
646
|
+
case "range": {
|
|
647
|
+
const indices = applyRangeIndices(items.length, config.rangeFrom, config.rangeTo, config.rangeStep)
|
|
648
|
+
return splitByIndices(items, indices)
|
|
649
|
+
}
|
|
650
|
+
case "list": {
|
|
651
|
+
const indices = resolveListExpression(config.listExpression ?? "", items.length)
|
|
652
|
+
return splitByIndices(items, indices)
|
|
653
|
+
}
|
|
654
|
+
case "random":
|
|
655
|
+
return selectRandom(items, config.randomCount ?? 1, config.seed)
|
|
656
|
+
case "modulo":
|
|
657
|
+
return selectByModulo(items, config.moduloDivisor)
|
|
658
|
+
case "predicate":
|
|
659
|
+
return selectByPredicate(items, config)
|
|
660
|
+
case "named-key":
|
|
661
|
+
return selectByNamedKey(items, config.namedKeyField, config.namedKeyValue)
|
|
662
|
+
default:
|
|
663
|
+
return { picked: [], rest: items.slice() }
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Partition `items` into picked (indices ∈ pickedIndices, in their original
|
|
669
|
+
* order in the source list) and rest (everything else). Deduplicates
|
|
670
|
+
* `pickedIndices` so passing the same index twice doesn't double-count.
|
|
671
|
+
*/
|
|
672
|
+
function splitByIndices(items: string[], pickedIndices: number[]): SelectorResult {
|
|
673
|
+
const pickedSet = new Set(pickedIndices.filter((i) => i >= 0 && i < items.length))
|
|
674
|
+
const picked: string[] = []
|
|
675
|
+
const rest: string[] = []
|
|
676
|
+
for (let i = 0; i < items.length; i++) {
|
|
677
|
+
if (pickedSet.has(i)) picked.push(items[i])
|
|
678
|
+
else rest.push(items[i])
|
|
679
|
+
}
|
|
680
|
+
return { picked, rest }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Returns the indices that `applyRange` would pick. Extracted so `runSelector`
|
|
685
|
+
* range mode can compute the exact picked-index set (`items.indexOf(value)`
|
|
686
|
+
* would return the wrong duplicate when the list contains repeated values).
|
|
687
|
+
*/
|
|
688
|
+
export function applyRangeIndices(
|
|
689
|
+
length: number,
|
|
690
|
+
from?: string,
|
|
691
|
+
to?: string,
|
|
692
|
+
step?: number,
|
|
693
|
+
): number[] {
|
|
694
|
+
if (length === 0) return []
|
|
695
|
+
const fromIdx = resolveIndex(from ?? "1", length, "1")
|
|
696
|
+
const toIdx = resolveIndex(to ?? "last", length, "last")
|
|
697
|
+
const effectiveStep = step === 0 || step === undefined ? 1 : step
|
|
698
|
+
if (effectiveStep > 0 && fromIdx > toIdx) return []
|
|
699
|
+
if (effectiveStep < 0 && fromIdx < toIdx) return []
|
|
700
|
+
const result: number[] = []
|
|
701
|
+
if (effectiveStep > 0) {
|
|
702
|
+
for (let i = fromIdx; i <= toIdx; i += effectiveStep) result.push(i)
|
|
703
|
+
} else {
|
|
704
|
+
for (let i = fromIdx; i >= toIdx; i += effectiveStep) result.push(i)
|
|
705
|
+
}
|
|
706
|
+
return result
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** mulberry32 PRNG — deterministic given a seed (any 32-bit signed integer). */
|
|
710
|
+
function mulberry32(seed: number): () => number {
|
|
711
|
+
let a = seed | 0
|
|
712
|
+
return () => {
|
|
713
|
+
a = (a + 0x6D2B79F5) | 0
|
|
714
|
+
let t = a
|
|
715
|
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
716
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
717
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** Hash a seed string to a 32-bit integer (FNV-1a variant). */
|
|
722
|
+
function hashSeed(s: string): number {
|
|
723
|
+
let h = 2166136261 | 0
|
|
724
|
+
for (let i = 0; i < s.length; i++) {
|
|
725
|
+
h ^= s.charCodeAt(i)
|
|
726
|
+
h = Math.imul(h, 16777619)
|
|
727
|
+
}
|
|
728
|
+
return h
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Pick `count` items uniformly at random without replacement.
|
|
733
|
+
* If `seed` is non-empty, the PRNG is seeded deterministically.
|
|
734
|
+
* Otherwise `Math.random` is used (fresh randomness per call).
|
|
735
|
+
*/
|
|
736
|
+
export function selectRandom(items: string[], count: number, seed?: string): SelectorResult {
|
|
737
|
+
if (items.length === 0) return { picked: [], rest: [] }
|
|
738
|
+
const k = Math.max(0, Math.min(count, items.length))
|
|
739
|
+
const rand = seed && seed.length > 0 ? mulberry32(hashSeed(seed)) : Math.random
|
|
740
|
+
// Partial Fisher-Yates: shuffle the first k positions, pick those indices.
|
|
741
|
+
const indices = items.map((_, i) => i)
|
|
742
|
+
for (let i = 0; i < k; i++) {
|
|
743
|
+
const j = i + Math.floor(rand() * (indices.length - i))
|
|
744
|
+
;[indices[i], indices[j]] = [indices[j], indices[i]]
|
|
745
|
+
}
|
|
746
|
+
return splitByIndices(items, indices.slice(0, k))
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Pick a single item at `divisor % items.length`. Non-numeric `divisor`
|
|
751
|
+
* falls back to index 0. Caller is responsible for resolving any
|
|
752
|
+
* `{NodeLabel}` template refs in `divisor` BEFORE calling this — see
|
|
753
|
+
* `resolveSelectorRefs`.
|
|
754
|
+
*/
|
|
755
|
+
export function selectByModulo(items: string[], divisor: string | undefined): SelectorResult {
|
|
756
|
+
if (items.length === 0) return { picked: [], rest: [] }
|
|
757
|
+
const n = divisor ? Number.parseInt(divisor.trim(), 10) : NaN
|
|
758
|
+
const safeN = Number.isFinite(n) ? n : 0
|
|
759
|
+
const idx = ((safeN % items.length) + items.length) % items.length
|
|
760
|
+
return splitByIndices(items, [idx])
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Pick items whose parsed JSON satisfies the predicate. `match: "first"`
|
|
765
|
+
* (default) stops at the first hit; `"all"` collects every hit.
|
|
766
|
+
*
|
|
767
|
+
* Reuses the filter-list condition evaluator so operator semantics
|
|
768
|
+
* (numeric coercion on `=`/`!=`, locale-aware ordering, case-sensitive
|
|
769
|
+
* text ops) stay identical to the Filter List node.
|
|
770
|
+
*/
|
|
771
|
+
export function selectByPredicate(
|
|
772
|
+
items: string[],
|
|
773
|
+
config: SelectorConfig,
|
|
774
|
+
): SelectorResult {
|
|
775
|
+
if (items.length === 0) return { picked: [], rest: [] }
|
|
776
|
+
// Default operator to "=" so the UI's visual default (selector-config.tsx
|
|
777
|
+
// shows `op={config.predicateOp ?? "="}`) matches runtime; first-time
|
|
778
|
+
// predicate use without explicitly setting an operator used to silently
|
|
779
|
+
// return picked=[].
|
|
780
|
+
// predicateField is intentionally allowed to be empty/undefined —
|
|
781
|
+
// evaluateCondition treats an empty path as "compare against the whole
|
|
782
|
+
// item" (filter-condition.ts:161), matching the UI placeholder hint
|
|
783
|
+
// "blank = whole item" on the field input.
|
|
784
|
+
const op = config.predicateOp ?? "="
|
|
785
|
+
// Pass caseSensitive explicitly. evaluateCondition defaults undefined to
|
|
786
|
+
// `true` (case-sensitive) to preserve legacy callers, but the selector
|
|
787
|
+
// UI checkbox shows unchecked by default — so undefined config must map
|
|
788
|
+
// to case-INsensitive at runtime to match what the user sees.
|
|
789
|
+
const opts = { caseSensitive: config.predicateCaseSensitive ?? false }
|
|
790
|
+
const matchAll = config.predicateMatch === "all"
|
|
791
|
+
const cond = {
|
|
792
|
+
id: "selector-predicate",
|
|
793
|
+
field: config.predicateField ?? "",
|
|
794
|
+
operator: op,
|
|
795
|
+
value: config.predicateValue ?? "",
|
|
796
|
+
valueType: "static" as const,
|
|
797
|
+
}
|
|
798
|
+
const matchedIndices: number[] = []
|
|
799
|
+
for (let i = 0; i < items.length; i++) {
|
|
800
|
+
const parsed = tryParseJson(items[i])
|
|
801
|
+
const hit = evaluateCondition(parsed, items[i], cond, undefined, opts)
|
|
802
|
+
if (hit) {
|
|
803
|
+
matchedIndices.push(i)
|
|
804
|
+
if (!matchAll) break
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return splitByIndices(items, matchedIndices)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Shortcut for `predicate(op="=", match="first")` — pick the first item
|
|
812
|
+
* whose `field` strictly equals `value`. Implemented as a thin wrapper
|
|
813
|
+
* so the UI can offer a 2-field form ("look up by name") instead of the
|
|
814
|
+
* full 4-field predicate form.
|
|
815
|
+
*/
|
|
816
|
+
export function selectByNamedKey(
|
|
817
|
+
items: string[],
|
|
818
|
+
field: string | undefined,
|
|
819
|
+
value: string | undefined,
|
|
820
|
+
): SelectorResult {
|
|
821
|
+
return selectByPredicate(items, {
|
|
822
|
+
mode: "predicate",
|
|
823
|
+
predicateField: field,
|
|
824
|
+
predicateOp: "=",
|
|
825
|
+
predicateValue: value,
|
|
826
|
+
predicateMatch: "first",
|
|
827
|
+
})
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Returns a new SelectorConfig with `{NodeLabel}` refs and `{{trigger.*}}` /
|
|
832
|
+
* `{{now}}` / `{{last_N_*:N}}` template tokens resolved in the four
|
|
833
|
+
* template-aware fields: moduloDivisor, predicateValue, namedKeyValue, seed.
|
|
834
|
+
* All other fields are passed through unchanged. Callers must build
|
|
835
|
+
* `variables` via `buildConditionVariables` (or equivalent) BEFORE calling.
|
|
836
|
+
*
|
|
837
|
+
* Other selector fields (itemIndex, range/list expressions, randomCount, etc.)
|
|
838
|
+
* are treated as literals — they have their own selector-lib syntax and are
|
|
839
|
+
* not resolved against the variable space.
|
|
840
|
+
*
|
|
841
|
+
* Token resolution matches `resolveConditionValue` exactly (both reuse
|
|
842
|
+
* `substituteTriggerTokens`) so the selector's `{{trigger.foo}}` semantics
|
|
843
|
+
* stay in lockstep with filter-list / router conditions.
|
|
844
|
+
*
|
|
845
|
+
* Pure: returns a new object; the input config is not mutated.
|
|
846
|
+
*/
|
|
847
|
+
export function resolveSelectorRefs(
|
|
848
|
+
config: SelectorConfig,
|
|
849
|
+
triggerData: Record<string, unknown> | undefined,
|
|
850
|
+
variables: ReadonlyMap<string, string>,
|
|
851
|
+
): SelectorConfig {
|
|
852
|
+
const resolve = (v: string | undefined): string | undefined => {
|
|
853
|
+
if (v == null) return v
|
|
854
|
+
const refsResolved = resolveNodeRefs(v, variables)
|
|
855
|
+
return substituteTriggerTokens(refsResolved, triggerData)
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
...config,
|
|
859
|
+
moduloDivisor: resolve(config.moduloDivisor),
|
|
860
|
+
predicateValue: resolve(config.predicateValue),
|
|
861
|
+
namedKeyValue: resolve(config.namedKeyValue),
|
|
862
|
+
seed: resolve(config.seed),
|
|
863
|
+
}
|
|
864
|
+
}
|