zyzz 0.0.9 → 0.0.12

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.
Files changed (119) hide show
  1. package/README.md +1 -1
  2. package/dist/Style.js +2 -2
  3. package/dist/Style.js.map +1 -1
  4. package/dist/Vars.d.ts +2 -2
  5. package/dist/Vars.d.ts.map +1 -1
  6. package/dist/Vars.js.map +1 -1
  7. package/dist/compiler/Source.d.ts.map +1 -1
  8. package/dist/compiler/Source.js +10 -0
  9. package/dist/compiler/Source.js.map +1 -1
  10. package/dist/compiler/internal/Contract.d.ts.map +1 -1
  11. package/dist/compiler/internal/Contract.js +11 -6
  12. package/dist/compiler/internal/Contract.js.map +1 -1
  13. package/dist/compiler/internal/Contributions.d.ts.map +1 -1
  14. package/dist/compiler/internal/Contributions.js +592 -556
  15. package/dist/compiler/internal/Contributions.js.map +1 -1
  16. package/dist/compiler/internal/Expression.d.ts +4 -0
  17. package/dist/compiler/internal/Expression.d.ts.map +1 -1
  18. package/dist/compiler/internal/Expression.js +45 -0
  19. package/dist/compiler/internal/Expression.js.map +1 -1
  20. package/dist/compiler/internal/RecipeConditions.js +2 -2
  21. package/dist/compiler/internal/RecipeConditions.js.map +1 -1
  22. package/dist/compiler/internal/Themes.d.ts.map +1 -1
  23. package/dist/compiler/internal/Themes.js +2 -0
  24. package/dist/compiler/internal/Themes.js.map +1 -1
  25. package/dist/default.d.ts +75 -19
  26. package/dist/default.d.ts.map +1 -1
  27. package/dist/default.js +76 -11
  28. package/dist/default.js.map +2 -2
  29. package/dist/default.js.zyzz.json +1 -1
  30. package/dist/internal/Condition.d.ts +1 -1
  31. package/dist/internal/Condition.d.ts.map +1 -1
  32. package/dist/internal/Configuration.js +2 -2
  33. package/dist/internal/Configuration.js.map +1 -1
  34. package/dist/internal/Query.d.ts +2 -2
  35. package/dist/internal/Query.d.ts.map +1 -1
  36. package/dist/internal/Query.js +1 -1
  37. package/dist/internal/Query.js.map +1 -1
  38. package/dist/internal/Theme.d.ts +4 -4
  39. package/dist/internal/Theme.d.ts.map +1 -1
  40. package/dist/internal/Theme.js +6 -6
  41. package/dist/internal/Theme.js.map +1 -1
  42. package/dist/internal/Token.d.ts +185 -2
  43. package/dist/internal/Token.d.ts.map +1 -1
  44. package/dist/internal/Token.js +236 -23
  45. package/dist/internal/Token.js.map +1 -1
  46. package/dist/internal/VariableSets.d.ts.map +1 -1
  47. package/dist/internal/VariableSets.js +9 -6
  48. package/dist/internal/VariableSets.js.map +1 -1
  49. package/dist/unplugin/index.d.ts.map +1 -1
  50. package/dist/unplugin/index.js +14 -0
  51. package/dist/unplugin/index.js.map +1 -1
  52. package/dist/variants.d.ts +13 -2
  53. package/dist/variants.d.ts.map +1 -1
  54. package/dist/variants.js.map +1 -1
  55. package/dist/web/colorProfile.d.ts +9 -5
  56. package/dist/web/colorProfile.d.ts.map +1 -1
  57. package/dist/web/colorProfile.js.map +1 -1
  58. package/dist/web/counterStyle.d.ts +13 -9
  59. package/dist/web/counterStyle.d.ts.map +1 -1
  60. package/dist/web/counterStyle.js.map +1 -1
  61. package/dist/web/cssFunction.d.ts +19 -14
  62. package/dist/web/cssFunction.d.ts.map +1 -1
  63. package/dist/web/cssFunction.js.map +1 -1
  64. package/dist/web/fontFace.d.ts +6 -1
  65. package/dist/web/fontFace.d.ts.map +1 -1
  66. package/dist/web/fontFace.js.map +1 -1
  67. package/dist/web/fontFeatureValues.d.ts +11 -7
  68. package/dist/web/fontFeatureValues.d.ts.map +1 -1
  69. package/dist/web/fontFeatureValues.js.map +1 -1
  70. package/dist/web/fontPaletteValues.d.ts +8 -3
  71. package/dist/web/fontPaletteValues.d.ts.map +1 -1
  72. package/dist/web/fontPaletteValues.js.map +1 -1
  73. package/dist/web/internal/Context.d.ts +3 -3
  74. package/dist/web/internal/Context.d.ts.map +1 -1
  75. package/dist/web/keyframes.d.ts +7 -3
  76. package/dist/web/keyframes.d.ts.map +1 -1
  77. package/dist/web/keyframes.js.map +1 -1
  78. package/dist/web/page.d.ts +11 -4
  79. package/dist/web/page.d.ts.map +1 -1
  80. package/dist/web/page.js.map +1 -1
  81. package/dist/web/positionTry.d.ts +6 -1
  82. package/dist/web/positionTry.d.ts.map +1 -1
  83. package/dist/web/positionTry.js.map +1 -1
  84. package/dist/web/property.d.ts +10 -5
  85. package/dist/web/property.d.ts.map +1 -1
  86. package/dist/web/property.js.map +1 -1
  87. package/dist/web/viewTransition.d.ts +10 -6
  88. package/dist/web/viewTransition.d.ts.map +1 -1
  89. package/dist/web/viewTransition.js.map +1 -1
  90. package/package.json +1 -1
  91. package/src/Style.ts +2 -2
  92. package/src/Vars.ts +2 -3
  93. package/src/compiler/Source.ts +19 -0
  94. package/src/compiler/internal/Contract.ts +19 -8
  95. package/src/compiler/internal/Contributions.ts +734 -685
  96. package/src/compiler/internal/Expression.ts +57 -0
  97. package/src/compiler/internal/RecipeConditions.ts +2 -2
  98. package/src/compiler/internal/Themes.ts +2 -0
  99. package/src/default.ts +175 -30
  100. package/src/internal/Condition.ts +3 -3
  101. package/src/internal/Configuration.ts +2 -2
  102. package/src/internal/Query.ts +3 -3
  103. package/src/internal/Theme.ts +12 -12
  104. package/src/internal/Token.ts +253 -33
  105. package/src/internal/VariableSets.ts +28 -25
  106. package/src/unplugin/index.ts +14 -0
  107. package/src/variants.ts +35 -2
  108. package/src/web/colorProfile.ts +25 -10
  109. package/src/web/counterStyle.ts +29 -14
  110. package/src/web/cssFunction.ts +47 -24
  111. package/src/web/fontFace.ts +16 -3
  112. package/src/web/fontFeatureValues.ts +35 -18
  113. package/src/web/fontPaletteValues.ts +25 -11
  114. package/src/web/internal/Context.ts +9 -3
  115. package/src/web/keyframes.ts +24 -9
  116. package/src/web/page.ts +22 -5
  117. package/src/web/positionTry.ts +20 -5
  118. package/src/web/property.ts +23 -9
  119. package/src/web/viewTransition.ts +28 -13
@@ -663,740 +663,789 @@ export function extract(
663
663
  }
664
664
  }
665
665
 
666
- for (const call of scanned.calls) {
667
- const before = result.length
666
+ function definitions(
667
+ input: unknown,
668
+ headers: readonly string[] = [],
669
+ ): { input: unknown; headers: readonly string[] }[] {
670
+ const entries = Object.entries(record(input))
671
+ const groups = new Set(
672
+ entries
673
+ .filter(
674
+ ([key]) =>
675
+ key === '@layer' ||
676
+ /^@(media|supports|container|layer)(?=[\t\n\r\f (/])/.test(key),
677
+ )
678
+ .map(([key]) => key),
679
+ )
680
+ const descriptors = entries.filter(([key]) => !groups.has(key))
681
+ const body = Object.fromEntries(descriptors)
682
+ if (!entries.length) return [{ input: body, headers }]
683
+
684
+ // A complete descriptor block occupies its first descriptor's authored position.
685
+ return entries.flatMap(([header, child]) =>
686
+ groups.has(header)
687
+ ? definitions(child, [...headers, Condition.normalize(header)])
688
+ : header === descriptors[0]?.[0]
689
+ ? [{ input: body, headers }]
690
+ : [],
691
+ )
692
+ }
668
693
 
694
+ for (const call of scanned.calls) {
669
695
  try {
670
- const input = value(call.argument)
696
+ const argument = value(call.argument)
671
697
  if (call.name && !call.exported && !scanned.used.has(call.name)) continue
672
- if (call.kind === 'importCss') {
673
- const options = record(input)
674
- if (
675
- Object.keys(options).some(
676
- (key) => !['layer', 'media', 'supports', 'url'].includes(key),
677
- ) ||
678
- typeof options.url !== 'string' ||
679
- Object.entries(options).some(
680
- ([key, value]) =>
681
- key !== 'url' &&
682
- value !== undefined &&
683
- typeof value !== 'string' &&
684
- !(key === 'layer' && value === true),
685
- )
686
- )
687
- throw new Error(
688
- 'Expected a stylesheet URL and static import conditions.',
689
- )
690
- result.push({
691
- kind: 'import',
692
- url: options.url,
693
- ...(options.layer !== undefined
694
- ? { layer: options.layer as string | true }
695
- : {}),
696
- ...(options.supports !== undefined
697
- ? { supports: options.supports as string }
698
- : {}),
699
- ...(options.media !== undefined
700
- ? { media: options.media as string }
701
- : {}),
702
- })
703
- } else if (call.kind === 'namespace') {
704
- const options = record(input)
705
- if (
706
- Object.keys(options).some(
707
- (key) => !['prefix', 'uri'].includes(key),
708
- ) ||
709
- typeof options.uri !== 'string' ||
710
- (options.prefix !== undefined &&
711
- (typeof options.prefix !== 'string' ||
712
- Identifiers.read(options.prefix) === undefined))
698
+ const contextValue = call.context ? value(call.context) : undefined
699
+ if (
700
+ contextValue !== undefined &&
701
+ Object.keys(record(contextValue)).some((key) => key !== 'id')
702
+ )
703
+ throw new Error(
704
+ 'Unknown contribution context option. Use nested at-rule keys for enclosing groups.',
713
705
  )
714
- throw new Error(
715
- 'Expected a namespace URI and optional identifier prefix.',
706
+
707
+ const inputs = [
708
+ 'importCss',
709
+ 'namespace',
710
+ 'customMedia',
711
+ 'layers',
712
+ 'global',
713
+ ].includes(call.kind)
714
+ ? [{ input: argument, headers: [] }]
715
+ : definitions(argument)
716
+ let functionSignature: string | undefined
717
+ for (const { input, headers } of inputs) {
718
+ const before = result.length
719
+ if (call.kind === 'importCss') {
720
+ const options = record(input)
721
+ if (
722
+ Object.keys(options).some(
723
+ (key) => !['layer', 'media', 'supports', 'url'].includes(key),
724
+ ) ||
725
+ typeof options.url !== 'string' ||
726
+ Object.entries(options).some(
727
+ ([key, value]) =>
728
+ key !== 'url' &&
729
+ value !== undefined &&
730
+ typeof value !== 'string' &&
731
+ !(key === 'layer' && value === true),
732
+ )
716
733
  )
717
- result.push({
718
- kind: 'namespace',
719
- uri: options.uri,
720
- ...(options.prefix !== undefined
721
- ? { prefix: options.prefix as string }
722
- : {}),
723
- name: `z-n${scanned.namespace}-${call.start.toString(36)}`,
724
- })
725
- } else if (call.kind === 'customMedia') {
726
- if (typeof input !== 'string' && typeof input !== 'boolean')
727
- throw new Error('Expected a media query or boolean.')
728
- if (typeof input === 'string') Descriptors.media(input)
729
- result.push({ kind: 'custom-media', name: call.name!, query: input })
730
- } else if (call.kind === 'cssFunction') {
731
- const options = record(input)
732
- if (
733
- Object.keys(options).some(
734
- (key) => !['body', 'parameters', 'returns'].includes(key),
735
- ) ||
736
- !Array.isArray(options.parameters) ||
737
- (options.returns !== undefined &&
738
- (typeof options.returns !== 'string' ||
739
- !FunctionSyntax.accepts(options.returns)))
740
- )
741
- throw new Error(
742
- 'Expected CSS function parameters, body, and optional return syntax.',
734
+ throw new Error(
735
+ 'Expected a stylesheet URL and static import conditions.',
736
+ )
737
+ result.push({
738
+ kind: 'import',
739
+ url: options.url,
740
+ ...(options.layer !== undefined
741
+ ? { layer: options.layer as string | true }
742
+ : {}),
743
+ ...(options.supports !== undefined
744
+ ? { supports: options.supports as string }
745
+ : {}),
746
+ ...(options.media !== undefined
747
+ ? { media: options.media as string }
748
+ : {}),
749
+ })
750
+ } else if (call.kind === 'namespace') {
751
+ const options = record(input)
752
+ if (
753
+ Object.keys(options).some(
754
+ (key) => !['prefix', 'uri'].includes(key),
755
+ ) ||
756
+ typeof options.uri !== 'string' ||
757
+ (options.prefix !== undefined &&
758
+ (typeof options.prefix !== 'string' ||
759
+ Identifiers.read(options.prefix) === undefined))
743
760
  )
744
- const names = new Set<string>()
745
- const parameters = options.parameters.map((input) => {
746
- const parameter = record(input)
761
+ throw new Error(
762
+ 'Expected a namespace URI and optional identifier prefix.',
763
+ )
764
+ result.push({
765
+ kind: 'namespace',
766
+ uri: options.uri,
767
+ ...(options.prefix !== undefined
768
+ ? { prefix: options.prefix as string }
769
+ : {}),
770
+ name: `z-n${scanned.namespace}-${call.start.toString(36)}`,
771
+ })
772
+ } else if (call.kind === 'customMedia') {
773
+ if (typeof input !== 'string' && typeof input !== 'boolean')
774
+ throw new Error('Expected a media query or boolean.')
775
+ if (typeof input === 'string') Descriptors.media(input)
776
+ result.push({ kind: 'custom-media', name: call.name!, query: input })
777
+ } else if (call.kind === 'cssFunction') {
778
+ const options = record(input)
747
779
  if (
748
- Object.keys(parameter).some(
749
- (key) => !['default', 'name', 'syntax'].includes(key),
780
+ Object.keys(options).some(
781
+ (key) => !['body', 'parameters', 'returns'].includes(key),
750
782
  ) ||
751
- typeof parameter.name !== 'string' ||
752
- !dashed(parameter.name) ||
753
- names.has(Identifiers.read(parameter.name)!) ||
754
- (parameter.syntax !== undefined &&
755
- (typeof parameter.syntax !== 'string' ||
756
- !FunctionSyntax.accepts(parameter.syntax))) ||
757
- (parameter.default !== undefined &&
758
- typeof parameter.default !== 'string' &&
759
- typeof parameter.default !== 'number')
783
+ !Array.isArray(options.parameters) ||
784
+ (options.returns !== undefined &&
785
+ (typeof options.returns !== 'string' ||
786
+ !FunctionSyntax.accepts(options.returns)))
760
787
  )
761
788
  throw new Error(
762
- 'Expected unique CSS parameters with supported syntaxes and scalar defaults.',
789
+ 'Expected CSS function parameters, body, and optional return syntax.',
763
790
  )
764
- names.add(Identifiers.read(parameter.name)!)
765
- if (parameter.default !== undefined)
766
- Functions.value(
767
- String(parameter.syntax ?? '*'),
768
- String(parameter.default),
791
+ const names = new Set<string>()
792
+ const parameters = options.parameters.map((input) => {
793
+ const parameter = record(input)
794
+ if (
795
+ Object.keys(parameter).some(
796
+ (key) => !['default', 'name', 'syntax'].includes(key),
797
+ ) ||
798
+ typeof parameter.name !== 'string' ||
799
+ !dashed(parameter.name) ||
800
+ names.has(Identifiers.read(parameter.name)!) ||
801
+ (parameter.syntax !== undefined &&
802
+ (typeof parameter.syntax !== 'string' ||
803
+ !FunctionSyntax.accepts(parameter.syntax))) ||
804
+ (parameter.default !== undefined &&
805
+ typeof parameter.default !== 'string' &&
806
+ typeof parameter.default !== 'number')
769
807
  )
770
- return `${parameter.name}${parameter.syntax ? ` ${parameter.syntax === '*' ? 'type(*)' : parameter.syntax}` : ''}${parameter.default !== undefined ? `: ${parameter.default}` : ''}`
771
- })
772
- function body(input: unknown): readonly Block.Entry[] {
773
- return Object.entries(record(input)).flatMap(
774
- ([key, value]): Block.Entry[] => {
775
- if (value === undefined) return []
776
- if (/^@(media|supports|container) /.test(key))
777
- return [
778
- {
779
- kind: 'block',
780
- header: Condition.normalize(key),
781
- entries: body(value),
782
- },
783
- ]
784
- if (
785
- (key !== 'result' && !dashed(key)) ||
786
- (typeof value !== 'string' && typeof value !== 'number')
808
+ throw new Error(
809
+ 'Expected unique CSS parameters with supported syntaxes and scalar defaults.',
787
810
  )
788
- throw new Error(
789
- 'CSS function bodies accept result, local variables, and conditional groups.',
811
+ names.add(Identifiers.read(parameter.name)!)
812
+ if (parameter.default !== undefined)
813
+ Functions.value(
814
+ String(parameter.syntax ?? '*'),
815
+ String(parameter.default),
816
+ )
817
+ return `${parameter.name}${parameter.syntax ? ` ${parameter.syntax === '*' ? 'type(*)' : parameter.syntax}` : ''}${parameter.default !== undefined ? `: ${parameter.default}` : ''}`
818
+ })
819
+ function body(input: unknown): readonly Block.Entry[] {
820
+ return Object.entries(record(input)).flatMap(
821
+ ([key, value]): Block.Entry[] => {
822
+ if (value === undefined) return []
823
+ if (/^@(media|supports|container) /.test(key))
824
+ return [
825
+ {
826
+ kind: 'block',
827
+ header: Condition.normalize(key),
828
+ entries: body(value),
829
+ },
830
+ ]
831
+ if (
832
+ (key !== 'result' && !dashed(key)) ||
833
+ (typeof value !== 'string' && typeof value !== 'number')
790
834
  )
791
- Functions.value('*', String(value))
792
- return [{ kind: 'descriptor', name: key, value }]
793
- },
835
+ throw new Error(
836
+ 'CSS function bodies accept result, local variables, and conditional groups.',
837
+ )
838
+ Functions.value('*', String(value))
839
+ return [{ kind: 'descriptor', name: key, value }]
840
+ },
841
+ )
842
+ }
843
+ const signature = JSON.stringify({
844
+ parameters: options.parameters.map((input) => {
845
+ const parameter = record(input)
846
+ return [
847
+ parameter.name,
848
+ parameter.syntax ?? '*',
849
+ parameter.default,
850
+ ]
851
+ }),
852
+ returns: options.returns ?? '*',
853
+ })
854
+ if (
855
+ functionSignature !== undefined &&
856
+ functionSignature !== signature
794
857
  )
795
- }
796
- call.function!.parameters =
797
- options.parameters as readonly cssFunction.Parameter[]
798
- call.function!.returns = (options.returns ?? '*') as cssFunction.Syntax
799
- result.push({
800
- kind: 'block',
801
- header: `@function ${call.name}(${parameters.join(',')})${options.returns ? ` returns ${options.returns === '*' ? 'type(*)' : options.returns}` : ''}`,
802
- entries: body(options.body),
803
- })
804
- } else if (call.kind === 'layers') {
805
- if (
806
- !Array.isArray(input) ||
807
- input.some((value) => typeof value !== 'string')
808
- )
809
- throw new Error('Layers require a literal string list.')
810
-
811
- result.push({ kind: 'layers', names: input })
812
- } else if (call.kind === 'global') {
813
- for (const [selector, child] of Object.entries(record(input)))
858
+ throw new Error(
859
+ 'Grouped CSS function definitions must use the same parameters and return syntax.',
860
+ )
861
+ functionSignature = signature
862
+ call.function!.parameters =
863
+ options.parameters as readonly cssFunction.Parameter[]
864
+ call.function!.returns = (options.returns ??
865
+ '*') as cssFunction.Syntax
814
866
  result.push({
815
- kind: 'rule',
816
- selector,
817
- style: grouping(selector) ? global(child) : style(child),
867
+ kind: 'block',
868
+ header: `@function ${call.name}(${parameters.join(',')})${options.returns ? ` returns ${options.returns === '*' ? 'type(*)' : options.returns}` : ''}`,
869
+ entries: body(options.body),
818
870
  })
819
- } else if (call.kind === 'fontFace') {
820
- const declarations = record(input)
821
-
822
- for (const key of Object.keys(declarations))
871
+ } else if (call.kind === 'layers') {
823
872
  if (
824
- declarations[key] === undefined &&
825
- key !== 'fontFamily' &&
826
- key !== 'src'
827
- )
828
- delete declarations[key]
829
-
830
- const keys = [
831
- 'fontFeatureSettings',
832
- 'fontVariationSettings',
833
- 'fontFamily',
834
- 'src',
835
- 'fontDisplay',
836
- 'fontStyle',
837
- 'fontWeight',
838
- 'fontStretch',
839
- 'unicodeRange',
840
- 'sizeAdjust',
841
- 'ascentOverride',
842
- 'descentOverride',
843
- 'lineGapOverride',
844
- ]
845
- if (
846
- typeof declarations.fontFamily !== 'string' ||
847
- typeof declarations.src !== 'string' ||
848
- Object.entries(declarations).some(
849
- ([key, value]) =>
850
- !keys.includes(key) ||
851
- (typeof value !== 'string' && typeof value !== 'number'),
852
- )
853
- )
854
- throw new Error(
855
- 'Font faces require family/source and scalar supported descriptors.',
873
+ !Array.isArray(input) ||
874
+ input.some((value) => typeof value !== 'string')
856
875
  )
876
+ throw new Error('Layers require a literal string list.')
877
+
878
+ result.push({ kind: 'layers', names: input })
879
+ } else if (call.kind === 'global') {
880
+ for (const [selector, child] of Object.entries(record(input)))
881
+ result.push({
882
+ kind: 'rule',
883
+ selector,
884
+ style: grouping(selector) ? global(child) : style(child),
885
+ })
886
+ } else if (call.kind === 'fontFace') {
887
+ const declarations = record(input)
857
888
 
858
- for (const [name, value] of Object.entries(declarations))
859
- Descriptors.check({ rule: 'font-face', name, value: String(value) })
860
- result.push({
861
- kind: 'font-face',
862
- declarations: declarations as Record<string, string | number>,
863
- })
864
- } else if (call.kind === 'property') {
865
- const options = record(input)
866
- const names =
867
- typeof options.name === 'string'
868
- ? Identifiers.list(options.name, 'space')
869
- : undefined
870
- if (
871
- Object.keys(options).some(
872
- (key) =>
873
- !['name', 'syntax', 'inherits', 'initialValue'].includes(key),
874
- ) ||
875
- names?.length !== 1 ||
876
- !names[0]!.startsWith('--') ||
877
- names[0]!.length <= 2 ||
878
- typeof options.syntax !== 'string' ||
879
- typeof options.inherits !== 'boolean' ||
880
- (options.initialValue !== undefined &&
881
- typeof options.initialValue !== 'string' &&
882
- typeof options.initialValue !== 'number')
883
- )
884
- throw new Error(
885
- 'Expected a native property registration with scalar descriptors.',
889
+ for (const key of Object.keys(declarations))
890
+ if (
891
+ declarations[key] === undefined &&
892
+ key !== 'fontFamily' &&
893
+ key !== 'src'
894
+ )
895
+ delete declarations[key]
896
+
897
+ const keys = [
898
+ 'fontFeatureSettings',
899
+ 'fontVariationSettings',
900
+ 'fontFamily',
901
+ 'src',
902
+ 'fontDisplay',
903
+ 'fontStyle',
904
+ 'fontWeight',
905
+ 'fontStretch',
906
+ 'unicodeRange',
907
+ 'sizeAdjust',
908
+ 'ascentOverride',
909
+ 'descentOverride',
910
+ 'lineGapOverride',
911
+ ]
912
+ if (
913
+ typeof declarations.fontFamily !== 'string' ||
914
+ typeof declarations.src !== 'string' ||
915
+ Object.entries(declarations).some(
916
+ ([key, value]) =>
917
+ !keys.includes(key) ||
918
+ (typeof value !== 'string' && typeof value !== 'number'),
919
+ )
886
920
  )
887
- if (options.initialValue !== undefined)
888
- Properties.initial(String(options.initialValue))
889
- result.push({
890
- kind: 'property',
891
- name: options.name as `--${string}`,
892
- syntax: options.syntax,
893
- inherits: options.inherits,
894
- ...(options.initialValue !== undefined
895
- ? { initialValue: options.initialValue as string | number }
896
- : {}),
897
- })
898
- } else if (call.kind === 'page') {
899
- const options = record(input)
900
- if (
901
- Object.keys(options).some(
902
- (key) => !['descriptors', 'selector'].includes(key),
903
- ) ||
904
- (options.selector !== undefined &&
905
- typeof options.selector !== 'string')
906
- )
907
- throw new Error('Expected page descriptors and an optional selector.')
908
- const margins = [
909
- 'top-left-corner',
910
- 'top-left',
911
- 'top-center',
912
- 'top-right',
913
- 'top-right-corner',
914
- 'bottom-left-corner',
915
- 'bottom-left',
916
- 'bottom-center',
917
- 'bottom-right',
918
- 'bottom-right-corner',
919
- 'left-top',
920
- 'left-middle',
921
- 'left-bottom',
922
- 'right-top',
923
- 'right-middle',
924
- 'right-bottom',
925
- ].map((name) => `@${name}`)
926
- const properties =
927
- /^(?!border(?:Collapse|Spacing)$)(?:background|border|font|margin|padding|outline)/
928
- const names = [
929
- 'color',
930
- 'counterIncrement',
931
- 'counterReset',
932
- 'direction',
933
- 'height',
934
- 'letterSpacing',
935
- 'lineHeight',
936
- 'maxHeight',
937
- 'maxWidth',
938
- 'minHeight',
939
- 'minWidth',
940
- 'quotes',
941
- 'textAlign',
942
- 'textDecoration',
943
- 'textIndent',
944
- 'textTransform',
945
- 'visibility',
946
- 'whiteSpace',
947
- 'width',
948
- 'wordSpacing',
949
- ]
950
- function body(input: unknown, margin = false): readonly Block.Entry[] {
951
- return Object.entries(record(input)).flatMap(
952
- ([key, value]): Block.Entry[] => {
953
- if (value === undefined) return []
954
- if (!margin && margins.includes(key))
955
- return [
956
- { kind: 'block', header: key, entries: body(value, true) },
957
- ]
958
- if (
959
- !margin &&
960
- ['bleed', 'marks', 'pageOrientation', 'size'].includes(key)
961
- ) {
962
- if (typeof value !== 'string' && typeof value !== 'number')
963
- throw new Error('Expected a scalar page descriptor.')
964
- Descriptors.check({
965
- rule: 'page',
966
- name: key,
967
- value: String(value),
968
- })
969
- return [
970
- {
971
- kind: 'descriptor',
972
- name: key.replace(
973
- /[A-Z]/g,
974
- (letter) => `-${letter.toLowerCase()}`,
975
- ),
976
- value,
977
- },
978
- ]
979
- }
980
- if (
981
- !properties.test(key) &&
982
- !names.includes(key) &&
983
- !(
984
- margin &&
985
- [
986
- 'content',
987
- 'overflow',
988
- 'unicodeBidi',
989
- 'verticalAlign',
990
- 'zIndex',
991
- ].includes(key)
992
- )
993
- )
994
- throw new Error('Unsupported page or page-margin declaration.')
995
- return [{ kind: 'style', style: style({ [key]: value }) }]
996
- },
921
+ throw new Error(
922
+ 'Font faces require family/source and scalar supported descriptors.',
923
+ )
924
+
925
+ for (const [name, value] of Object.entries(declarations))
926
+ Descriptors.check({ rule: 'font-face', name, value: String(value) })
927
+ result.push({
928
+ kind: 'font-face',
929
+ declarations: declarations as Record<string, string | number>,
930
+ })
931
+ } else if (call.kind === 'property') {
932
+ const options = record(input)
933
+ const names =
934
+ typeof options.name === 'string'
935
+ ? Identifiers.list(options.name, 'space')
936
+ : undefined
937
+ if (
938
+ Object.keys(options).some(
939
+ (key) =>
940
+ !['name', 'syntax', 'inherits', 'initialValue'].includes(key),
941
+ ) ||
942
+ names?.length !== 1 ||
943
+ !names[0]!.startsWith('--') ||
944
+ names[0]!.length <= 2 ||
945
+ typeof options.syntax !== 'string' ||
946
+ typeof options.inherits !== 'boolean' ||
947
+ (options.initialValue !== undefined &&
948
+ typeof options.initialValue !== 'string' &&
949
+ typeof options.initialValue !== 'number')
997
950
  )
998
- }
999
- result.push({
1000
- kind: 'block',
1001
- header: Condition.normalize(
1002
- `@page${options.selector ? ` ${options.selector}` : ''}`,
1003
- ),
1004
- entries: body(options.descriptors),
1005
- })
1006
- } else if (call.kind === 'fontFeatureValues') {
1007
- const options = record(input)
1008
- if (
1009
- Object.keys(options).some(
1010
- (key) => !['families', 'features', 'fontDisplay'].includes(key),
951
+ throw new Error(
952
+ 'Expected a native property registration with scalar descriptors.',
953
+ )
954
+ if (options.initialValue !== undefined)
955
+ Properties.initial(String(options.initialValue))
956
+ result.push({
957
+ kind: 'property',
958
+ name: options.name as `--${string}`,
959
+ syntax: options.syntax,
960
+ inherits: options.inherits,
961
+ ...(options.initialValue !== undefined
962
+ ? { initialValue: options.initialValue as string | number }
963
+ : {}),
964
+ })
965
+ } else if (call.kind === 'page') {
966
+ const options = record(input)
967
+ if (
968
+ Object.keys(options).some(
969
+ (key) => !['descriptors', 'selector'].includes(key),
970
+ ) ||
971
+ (options.selector !== undefined &&
972
+ typeof options.selector !== 'string')
1011
973
  )
1012
- )
1013
- throw new Error('Unknown font-feature-values option.')
1014
- const families = options.families
1015
- if (
1016
- typeof families !== 'string' &&
1017
- (!Array.isArray(families) ||
1018
- !families.length ||
1019
- families.some((value) => typeof value !== 'string'))
1020
- )
1021
- throw new Error('Expected a font family list.')
1022
- const familyList = Array.isArray(families)
1023
- ? families
1024
- .map(
1025
- (value) =>
1026
- '"' +
1027
- Array.from(value as string, (char) => {
1028
- const code = char.codePointAt(0)!
1029
- return code < 32 ||
1030
- code === 127 ||
1031
- char === '"' ||
1032
- char === '\\'
1033
- ? `\\${code.toString(16)} `
1034
- : char
1035
- }).join('') +
1036
- '"',
1037
- )
1038
- .join(',')
1039
- : families
1040
- // Validate the native prelude before shielding newer body descriptors.
1041
- Lightning.transform({
1042
- filename: 'font-feature-values.css',
1043
- code: new TextEncoder().encode(
1044
- `@font-feature-values ${familyList} {}`,
1045
- ),
1046
- })
1047
- const entries: Block.Entry[] = []
1048
- for (const key of Object.keys(options)) {
1049
- if (key === 'fontDisplay' && options.fontDisplay !== undefined) {
1050
- if (
1051
- typeof options.fontDisplay !== 'string' ||
1052
- !['auto', 'block', 'fallback', 'optional', 'swap'].includes(
1053
- Identifiers.list(options.fontDisplay, 'space')
1054
- ?.join(' ')
1055
- .replace(/[A-Z]/g, (letter) => letter.toLowerCase()) ?? '',
1056
- )
974
+ throw new Error(
975
+ 'Expected page descriptors and an optional selector.',
976
+ )
977
+ const margins = [
978
+ 'top-left-corner',
979
+ 'top-left',
980
+ 'top-center',
981
+ 'top-right',
982
+ 'top-right-corner',
983
+ 'bottom-left-corner',
984
+ 'bottom-left',
985
+ 'bottom-center',
986
+ 'bottom-right',
987
+ 'bottom-right-corner',
988
+ 'left-top',
989
+ 'left-middle',
990
+ 'left-bottom',
991
+ 'right-top',
992
+ 'right-middle',
993
+ 'right-bottom',
994
+ ].map((name) => `@${name}`)
995
+ const properties =
996
+ /^(?!border(?:Collapse|Spacing)$)(?:background|border|font|margin|padding|outline)/
997
+ const names = [
998
+ 'color',
999
+ 'counterIncrement',
1000
+ 'counterReset',
1001
+ 'direction',
1002
+ 'height',
1003
+ 'letterSpacing',
1004
+ 'lineHeight',
1005
+ 'maxHeight',
1006
+ 'maxWidth',
1007
+ 'minHeight',
1008
+ 'minWidth',
1009
+ 'quotes',
1010
+ 'textAlign',
1011
+ 'textDecoration',
1012
+ 'textIndent',
1013
+ 'textTransform',
1014
+ 'visibility',
1015
+ 'whiteSpace',
1016
+ 'width',
1017
+ 'wordSpacing',
1018
+ ]
1019
+ function body(
1020
+ input: unknown,
1021
+ margin = false,
1022
+ ): readonly Block.Entry[] {
1023
+ return Object.entries(record(input)).flatMap(
1024
+ ([key, value]): Block.Entry[] => {
1025
+ if (value === undefined) return []
1026
+ if (!margin && margins.includes(key))
1027
+ return [
1028
+ { kind: 'block', header: key, entries: body(value, true) },
1029
+ ]
1030
+ if (
1031
+ !margin &&
1032
+ ['bleed', 'marks', 'pageOrientation', 'size'].includes(key)
1033
+ ) {
1034
+ if (typeof value !== 'string' && typeof value !== 'number')
1035
+ throw new Error('Expected a scalar page descriptor.')
1036
+ Descriptors.check({
1037
+ rule: 'page',
1038
+ name: key,
1039
+ value: String(value),
1040
+ })
1041
+ return [
1042
+ {
1043
+ kind: 'descriptor',
1044
+ name: key.replace(
1045
+ /[A-Z]/g,
1046
+ (letter) => `-${letter.toLowerCase()}`,
1047
+ ),
1048
+ value,
1049
+ },
1050
+ ]
1051
+ }
1052
+ if (
1053
+ !properties.test(key) &&
1054
+ !names.includes(key) &&
1055
+ !(
1056
+ margin &&
1057
+ [
1058
+ 'content',
1059
+ 'overflow',
1060
+ 'unicodeBidi',
1061
+ 'verticalAlign',
1062
+ 'zIndex',
1063
+ ].includes(key)
1064
+ )
1065
+ )
1066
+ throw new Error(
1067
+ 'Unsupported page or page-margin declaration.',
1068
+ )
1069
+ return [{ kind: 'style', style: style({ [key]: value }) }]
1070
+ },
1057
1071
  )
1058
- throw new Error('Invalid font display descriptor.')
1059
- entries.push({
1060
- kind: 'descriptor',
1061
- name: 'font-display',
1062
- value: options.fontDisplay,
1063
- })
1064
1072
  }
1065
- if (key !== 'features') continue
1066
- for (const [header, input] of Object.entries(
1067
- record(options.features),
1068
- )) {
1069
- if (input === undefined) continue
1070
- if (
1071
- ![
1072
- '@annotation',
1073
- '@character-variant',
1074
- '@ornaments',
1075
- '@styleset',
1076
- '@stylistic',
1077
- '@swash',
1078
- ].includes(header)
1073
+ result.push({
1074
+ kind: 'block',
1075
+ header: Condition.normalize(
1076
+ `@page${options.selector ? ` ${options.selector}` : ''}`,
1077
+ ),
1078
+ entries: body(options.descriptors),
1079
+ })
1080
+ } else if (call.kind === 'fontFeatureValues') {
1081
+ const options = record(input)
1082
+ if (
1083
+ Object.keys(options).some(
1084
+ (key) => !['families', 'features', 'fontDisplay'].includes(key),
1079
1085
  )
1080
- throw new Error('Unknown font feature block.')
1081
- const declarations: Block.Entry[] = Object.entries(
1082
- record(input),
1083
- ).map(([name, value]) => {
1084
- const values = Array.isArray(value) ? value : [value]
1085
- const maximum =
1086
- header === '@styleset'
1087
- ? Infinity
1088
- : header === '@character-variant'
1089
- ? 2
1090
- : 1
1091
- if (
1092
- !identifier(name) ||
1093
- !values.length ||
1094
- values.length > maximum ||
1095
- values.some(
1086
+ )
1087
+ throw new Error('Unknown font-feature-values option.')
1088
+ const families = options.families
1089
+ if (
1090
+ typeof families !== 'string' &&
1091
+ (!Array.isArray(families) ||
1092
+ !families.length ||
1093
+ families.some((value) => typeof value !== 'string'))
1094
+ )
1095
+ throw new Error('Expected a font family list.')
1096
+ const familyList = Array.isArray(families)
1097
+ ? families
1098
+ .map(
1096
1099
  (value) =>
1097
- typeof value !== 'number' ||
1098
- !Number.isSafeInteger(value) ||
1099
- value < 0,
1100
+ '"' +
1101
+ Array.from(value as string, (char) => {
1102
+ const code = char.codePointAt(0)!
1103
+ return code < 32 ||
1104
+ code === 127 ||
1105
+ char === '"' ||
1106
+ char === '\\'
1107
+ ? `\\${code.toString(16)} `
1108
+ : char
1109
+ }).join('') +
1110
+ '"',
1100
1111
  )
1101
- )
1102
- throw new Error(
1103
- 'Expected feature aliases with nonnegative integer indices.',
1112
+ .join(',')
1113
+ : families
1114
+ // Validate the native prelude before shielding newer body descriptors.
1115
+ Lightning.transform({
1116
+ filename: 'font-feature-values.css',
1117
+ code: new TextEncoder().encode(
1118
+ `@font-feature-values ${familyList} {}`,
1119
+ ),
1120
+ })
1121
+ const entries: Block.Entry[] = []
1122
+ for (const key of Object.keys(options)) {
1123
+ if (key === 'fontDisplay' && options.fontDisplay !== undefined) {
1124
+ if (
1125
+ typeof options.fontDisplay !== 'string' ||
1126
+ !['auto', 'block', 'fallback', 'optional', 'swap'].includes(
1127
+ Identifiers.list(options.fontDisplay, 'space')
1128
+ ?.join(' ')
1129
+ .replace(/[A-Z]/g, (letter) => letter.toLowerCase()) ?? '',
1104
1130
  )
1105
- return { kind: 'descriptor', name, value: values.join(' ') }
1106
- })
1107
- entries.push({ kind: 'block', header, entries: declarations })
1108
- }
1109
- }
1110
- result.push({
1111
- kind: 'block',
1112
- header: `@font-feature-values ${familyList}`,
1113
- entries,
1114
- })
1115
- } else if (call.kind === 'viewTransition') {
1116
- const entries = Object.entries(record(input)).flatMap(
1117
- ([key, value]): Block.Entry[] => {
1118
- if (value === undefined) return []
1119
- if (
1120
- (key !== 'navigation' && key !== 'types') ||
1121
- typeof value !== 'string' ||
1122
- (key === 'navigation' &&
1123
- !['auto', 'none'].includes(
1124
- Identifiers.list(value, 'space')?.join(' ').toLowerCase() ??
1125
- '',
1126
- ))
1127
- )
1128
- throw new Error(
1129
- 'Expected navigation or types view-transition descriptors.',
1130
1131
  )
1131
- if (key === 'types') {
1132
- const names = Identifiers.list(value, 'space')
1132
+ throw new Error('Invalid font display descriptor.')
1133
+ entries.push({
1134
+ kind: 'descriptor',
1135
+ name: 'font-display',
1136
+ value: options.fontDisplay,
1137
+ })
1138
+ }
1139
+ if (key !== 'features') continue
1140
+ for (const [header, input] of Object.entries(
1141
+ record(options.features),
1142
+ )) {
1143
+ if (input === undefined) continue
1133
1144
  if (
1134
- !names ||
1135
- names.some(
1136
- (name) =>
1137
- [
1138
- 'default',
1139
- 'inherit',
1140
- 'initial',
1141
- 'revert',
1142
- 'revert-layer',
1143
- 'unset',
1144
- ].includes(name.toLowerCase()) ||
1145
- (name.toLowerCase() === 'none' && names.length !== 1),
1145
+ ![
1146
+ '@annotation',
1147
+ '@character-variant',
1148
+ '@ornaments',
1149
+ '@styleset',
1150
+ '@stylistic',
1151
+ '@swash',
1152
+ ].includes(header)
1153
+ )
1154
+ throw new Error('Unknown font feature block.')
1155
+ const declarations: Block.Entry[] = Object.entries(
1156
+ record(input),
1157
+ ).map(([name, value]) => {
1158
+ const values = Array.isArray(value) ? value : [value]
1159
+ const maximum =
1160
+ header === '@styleset'
1161
+ ? Infinity
1162
+ : header === '@character-variant'
1163
+ ? 2
1164
+ : 1
1165
+ if (
1166
+ !identifier(name) ||
1167
+ !values.length ||
1168
+ values.length > maximum ||
1169
+ values.some(
1170
+ (value) =>
1171
+ typeof value !== 'number' ||
1172
+ !Number.isSafeInteger(value) ||
1173
+ value < 0,
1174
+ )
1146
1175
  )
1176
+ throw new Error(
1177
+ 'Expected feature aliases with nonnegative integer indices.',
1178
+ )
1179
+ return { kind: 'descriptor', name, value: values.join(' ') }
1180
+ })
1181
+ entries.push({ kind: 'block', header, entries: declarations })
1182
+ }
1183
+ }
1184
+ result.push({
1185
+ kind: 'block',
1186
+ header: `@font-feature-values ${familyList}`,
1187
+ entries,
1188
+ })
1189
+ } else if (call.kind === 'viewTransition') {
1190
+ const entries = Object.entries(record(input)).flatMap(
1191
+ ([key, value]): Block.Entry[] => {
1192
+ if (value === undefined) return []
1193
+ if (
1194
+ (key !== 'navigation' && key !== 'types') ||
1195
+ typeof value !== 'string' ||
1196
+ (key === 'navigation' &&
1197
+ !['auto', 'none'].includes(
1198
+ Identifiers.list(value, 'space')?.join(' ').toLowerCase() ??
1199
+ '',
1200
+ ))
1147
1201
  )
1148
1202
  throw new Error(
1149
- 'Expected none or a list of view-transition custom identifiers.',
1203
+ 'Expected navigation or types view-transition descriptors.',
1150
1204
  )
1151
- }
1152
- Functions.value('*', String(value))
1153
- return [{ kind: 'descriptor', name: key, value }]
1154
- },
1155
- )
1156
- result.push({ kind: 'block', header: '@view-transition', entries })
1157
- } else if (call.kind === 'positionTry') {
1158
- const declarations = record(input)
1159
- const keys = [
1160
- 'alignSelf',
1161
- 'blockSize',
1162
- 'bottom',
1163
- 'height',
1164
- 'inlineSize',
1165
- 'inset',
1166
- 'insetBlock',
1167
- 'insetBlockEnd',
1168
- 'insetBlockStart',
1169
- 'insetInline',
1170
- 'insetInlineEnd',
1171
- 'insetInlineStart',
1172
- 'justifySelf',
1173
- 'left',
1174
- 'margin',
1175
- 'marginBlock',
1176
- 'marginBlockEnd',
1177
- 'marginBlockStart',
1178
- 'marginBottom',
1179
- 'marginInline',
1180
- 'marginInlineEnd',
1181
- 'marginInlineStart',
1182
- 'marginLeft',
1183
- 'marginRight',
1184
- 'marginTop',
1185
- 'maxBlockSize',
1186
- 'maxHeight',
1187
- 'maxInlineSize',
1188
- 'maxWidth',
1189
- 'minBlockSize',
1190
- 'minHeight',
1191
- 'minInlineSize',
1192
- 'minWidth',
1193
- 'placeSelf',
1194
- 'positionAnchor',
1195
- 'positionArea',
1196
- 'right',
1197
- 'top',
1198
- 'width',
1199
- ]
1200
- if (Object.keys(declarations).some((key) => !keys.includes(key)))
1201
- throw new Error('Unsupported position-try declaration.')
1202
- const block = style(declarations)
1203
- if (block.rules || block.declarations.some((value) => value.important))
1204
- throw new Error(
1205
- 'Position-try forbids nested rules and important declarations.',
1206
- )
1207
- result.push({
1208
- kind: 'rule',
1209
- selector: `@position-try ${call.name}`,
1210
- style: block,
1211
- })
1212
- } else if (
1213
- call.kind === 'colorProfile' ||
1214
- call.kind === 'counterStyle' ||
1215
- call.kind === 'fontPaletteValues'
1216
- ) {
1217
- const descriptors = {
1218
- colorProfile: {
1219
- rule: 'color-profile',
1220
- keys: ['components', 'renderingIntent', 'src'],
1221
- required: ['src'],
1222
- },
1223
- counterStyle: {
1224
- rule: 'counter-style',
1225
- keys: [
1226
- 'additiveSymbols',
1227
- 'fallback',
1228
- 'negative',
1229
- 'pad',
1230
- 'prefix',
1231
- 'range',
1232
- 'speakAs',
1233
- 'suffix',
1234
- 'symbols',
1235
- 'system',
1236
- ],
1237
- required: [],
1238
- },
1239
- fontPaletteValues: {
1240
- rule: 'font-palette-values',
1241
- keys: ['basePalette', 'fontFamily', 'overrideColors'],
1242
- required: ['fontFamily'],
1243
- },
1244
- } as const
1245
- const definition = descriptors[call.kind]
1246
- const declarations = record(input)
1247
- for (const key of Object.keys(declarations))
1248
- if (declarations[key] === undefined) delete declarations[key]
1249
- if (
1250
- definition.required.some(
1251
- (key) => typeof declarations[key] !== 'string',
1252
- ) ||
1253
- Object.entries(declarations).some(
1254
- ([key, value]) =>
1255
- !(definition.keys as readonly string[]).includes(key) ||
1256
- (typeof value !== 'string' &&
1257
- !(
1258
- key === 'basePalette' &&
1259
- typeof value === 'number' &&
1260
- Number.isSafeInteger(value) &&
1261
- value >= 0
1262
- )),
1205
+ if (key === 'types') {
1206
+ const names = Identifiers.list(value, 'space')
1207
+ if (
1208
+ !names ||
1209
+ names.some(
1210
+ (name) =>
1211
+ [
1212
+ 'default',
1213
+ 'inherit',
1214
+ 'initial',
1215
+ 'revert',
1216
+ 'revert-layer',
1217
+ 'unset',
1218
+ ].includes(name.toLowerCase()) ||
1219
+ (name.toLowerCase() === 'none' && names.length !== 1),
1220
+ )
1221
+ )
1222
+ throw new Error(
1223
+ 'Expected none or a list of view-transition custom identifiers.',
1224
+ )
1225
+ }
1226
+ Functions.value('*', String(value))
1227
+ return [{ kind: 'descriptor', name: key, value }]
1228
+ },
1263
1229
  )
1264
- )
1265
- throw new Error(
1266
- 'Expected supported scalar descriptors and required fields.',
1230
+ result.push({ kind: 'block', header: '@view-transition', entries })
1231
+ } else if (call.kind === 'positionTry') {
1232
+ const declarations = record(input)
1233
+ const keys = [
1234
+ 'alignSelf',
1235
+ 'blockSize',
1236
+ 'bottom',
1237
+ 'height',
1238
+ 'inlineSize',
1239
+ 'inset',
1240
+ 'insetBlock',
1241
+ 'insetBlockEnd',
1242
+ 'insetBlockStart',
1243
+ 'insetInline',
1244
+ 'insetInlineEnd',
1245
+ 'insetInlineStart',
1246
+ 'justifySelf',
1247
+ 'left',
1248
+ 'margin',
1249
+ 'marginBlock',
1250
+ 'marginBlockEnd',
1251
+ 'marginBlockStart',
1252
+ 'marginBottom',
1253
+ 'marginInline',
1254
+ 'marginInlineEnd',
1255
+ 'marginInlineStart',
1256
+ 'marginLeft',
1257
+ 'marginRight',
1258
+ 'marginTop',
1259
+ 'maxBlockSize',
1260
+ 'maxHeight',
1261
+ 'maxInlineSize',
1262
+ 'maxWidth',
1263
+ 'minBlockSize',
1264
+ 'minHeight',
1265
+ 'minInlineSize',
1266
+ 'minWidth',
1267
+ 'placeSelf',
1268
+ 'positionAnchor',
1269
+ 'positionArea',
1270
+ 'right',
1271
+ 'top',
1272
+ 'width',
1273
+ ]
1274
+ if (Object.keys(declarations).some((key) => !keys.includes(key)))
1275
+ throw new Error('Unsupported position-try declaration.')
1276
+ const block = style(declarations)
1277
+ if (
1278
+ block.rules ||
1279
+ block.declarations.some((value) => value.important)
1267
1280
  )
1268
- if (call.kind === 'colorProfile') {
1269
- if (!Identifiers.url(declarations.src as string))
1270
- throw new Error('Color-profile src requires one URL.')
1271
- if (typeof declarations.components === 'string') {
1272
- const names = Identifiers.list(declarations.components, 'comma')
1273
- if (!names || names.some((name) => name.toLowerCase() === 'none'))
1274
- throw new Error(
1275
- 'Color-profile components require comma-separated identifiers other than none.',
1276
- )
1277
- }
1278
- if (typeof declarations.renderingIntent === 'string') {
1279
- const names = Identifiers.list(
1280
- declarations.renderingIntent,
1281
- 'space',
1282
- )
1283
- if (
1284
- names?.length !== 1 ||
1285
- ![
1286
- 'absolute-colorimetric',
1287
- 'relative-colorimetric',
1288
- 'perceptual',
1289
- 'saturation',
1290
- ].includes(names[0]!.toLowerCase())
1291
- )
1292
- throw new Error('Invalid color-profile rendering intent.')
1293
- }
1294
- }
1295
- if (call.kind === 'counterStyle') {
1296
- const system =
1297
- typeof declarations.system === 'string'
1298
- ? Lexical.normalize(declarations.system)
1299
- .trim()
1300
- .replace(/[A-Z]/g, (letter) => letter.toLowerCase())
1301
- : 'symbolic'
1302
- try {
1303
- Descriptors.check({
1304
- rule: 'counter-style',
1305
- name: 'system',
1306
- value:
1307
- typeof declarations.system === 'string'
1308
- ? declarations.system
1309
- : 'symbolic',
1310
- })
1311
- } catch {
1312
1281
  throw new Error(
1313
- 'Expected a supported counter system, with an integer after fixed.',
1282
+ 'Position-try forbids nested rules and important declarations.',
1314
1283
  )
1315
- }
1316
- const required =
1317
- system === 'additive'
1318
- ? 'additiveSymbols'
1319
- : system.startsWith('extends ')
1320
- ? undefined
1321
- : 'symbols'
1284
+ result.push({
1285
+ kind: 'rule',
1286
+ selector: `@position-try ${call.name}`,
1287
+ style: block,
1288
+ })
1289
+ } else if (
1290
+ call.kind === 'colorProfile' ||
1291
+ call.kind === 'counterStyle' ||
1292
+ call.kind === 'fontPaletteValues'
1293
+ ) {
1294
+ const descriptors = {
1295
+ colorProfile: {
1296
+ rule: 'color-profile',
1297
+ keys: ['components', 'renderingIntent', 'src'],
1298
+ required: ['src'],
1299
+ },
1300
+ counterStyle: {
1301
+ rule: 'counter-style',
1302
+ keys: [
1303
+ 'additiveSymbols',
1304
+ 'fallback',
1305
+ 'negative',
1306
+ 'pad',
1307
+ 'prefix',
1308
+ 'range',
1309
+ 'speakAs',
1310
+ 'suffix',
1311
+ 'symbols',
1312
+ 'system',
1313
+ ],
1314
+ required: [],
1315
+ },
1316
+ fontPaletteValues: {
1317
+ rule: 'font-palette-values',
1318
+ keys: ['basePalette', 'fontFamily', 'overrideColors'],
1319
+ required: ['fontFamily'],
1320
+ },
1321
+ } as const
1322
+ const definition = descriptors[call.kind]
1323
+ const declarations = record(input)
1324
+ for (const key of Object.keys(declarations))
1325
+ if (declarations[key] === undefined) delete declarations[key]
1322
1326
  if (
1323
- required &&
1324
- (typeof declarations[required] !== 'string' ||
1325
- !(declarations[required] as string).trim())
1327
+ definition.required.some(
1328
+ (key) => typeof declarations[key] !== 'string',
1329
+ ) ||
1330
+ Object.entries(declarations).some(
1331
+ ([key, value]) =>
1332
+ !(definition.keys as readonly string[]).includes(key) ||
1333
+ (typeof value !== 'string' &&
1334
+ !(
1335
+ key === 'basePalette' &&
1336
+ typeof value === 'number' &&
1337
+ Number.isSafeInteger(value) &&
1338
+ value >= 0
1339
+ )),
1340
+ )
1326
1341
  )
1327
1342
  throw new Error(
1328
- 'The counter system requires symbols or additiveSymbols.',
1343
+ 'Expected supported scalar descriptors and required fields.',
1329
1344
  )
1330
- }
1331
- if (call.kind !== 'colorProfile')
1332
- for (const [name, value] of Object.entries(declarations))
1333
- Descriptors.check({
1334
- rule: definition.rule,
1335
- name,
1336
- value: String(value),
1337
- })
1338
- result.push({
1339
- kind: 'descriptor',
1340
- rule: definition.rule,
1341
- name: call.name!,
1342
- declarations: declarations as Record<string, string | number>,
1343
- })
1344
- } else {
1345
- const frames = Object.entries(record(input)).map(([stop, input]) => {
1346
- if (!Keyframes.accepts(stop))
1347
- throw new Error(
1348
- 'Keyframe stops require from, to, 0–100% offsets, or named timeline percentages.',
1345
+ if (call.kind === 'colorProfile') {
1346
+ if (!Identifiers.url(declarations.src as string))
1347
+ throw new Error('Color-profile src requires one URL.')
1348
+ if (typeof declarations.components === 'string') {
1349
+ const names = Identifiers.list(declarations.components, 'comma')
1350
+ if (!names || names.some((name) => name.toLowerCase() === 'none'))
1351
+ throw new Error(
1352
+ 'Color-profile components require comma-separated identifiers other than none.',
1353
+ )
1354
+ }
1355
+ if (typeof declarations.renderingIntent === 'string') {
1356
+ const names = Identifiers.list(
1357
+ declarations.renderingIntent,
1358
+ 'space',
1359
+ )
1360
+ if (
1361
+ names?.length !== 1 ||
1362
+ ![
1363
+ 'absolute-colorimetric',
1364
+ 'relative-colorimetric',
1365
+ 'perceptual',
1366
+ 'saturation',
1367
+ ].includes(names[0]!.toLowerCase())
1368
+ )
1369
+ throw new Error('Invalid color-profile rendering intent.')
1370
+ }
1371
+ }
1372
+ if (call.kind === 'counterStyle') {
1373
+ const system =
1374
+ typeof declarations.system === 'string'
1375
+ ? Lexical.normalize(declarations.system)
1376
+ .trim()
1377
+ .replace(/[A-Z]/g, (letter) => letter.toLowerCase())
1378
+ : 'symbolic'
1379
+ try {
1380
+ Descriptors.check({
1381
+ rule: 'counter-style',
1382
+ name: 'system',
1383
+ value:
1384
+ typeof declarations.system === 'string'
1385
+ ? declarations.system
1386
+ : 'symbolic',
1387
+ })
1388
+ } catch {
1389
+ throw new Error(
1390
+ 'Expected a supported counter system, with an integer after fixed.',
1391
+ )
1392
+ }
1393
+ const required =
1394
+ system === 'additive'
1395
+ ? 'additiveSymbols'
1396
+ : system.startsWith('extends ')
1397
+ ? undefined
1398
+ : 'symbols'
1399
+ if (
1400
+ required &&
1401
+ (typeof declarations[required] !== 'string' ||
1402
+ !(declarations[required] as string).trim())
1349
1403
  )
1404
+ throw new Error(
1405
+ 'The counter system requires symbols or additiveSymbols.',
1406
+ )
1407
+ }
1408
+ if (call.kind !== 'colorProfile')
1409
+ for (const [name, value] of Object.entries(declarations))
1410
+ Descriptors.check({
1411
+ rule: definition.rule,
1412
+ name,
1413
+ value: String(value),
1414
+ })
1415
+ result.push({
1416
+ kind: 'descriptor',
1417
+ rule: definition.rule,
1418
+ name: call.name!,
1419
+ declarations: declarations as Record<string, string | number>,
1420
+ })
1421
+ } else {
1422
+ const frames = Object.entries(record(input)).map(([stop, input]) => {
1423
+ if (!Keyframes.accepts(stop))
1424
+ throw new Error(
1425
+ 'Keyframe stops require from, to, 0–100% offsets, or named timeline percentages.',
1426
+ )
1350
1427
 
1351
- const frame = style(input)
1352
- if (
1353
- frame.rules ||
1354
- frame.declarations.some((value) => value.important)
1355
- )
1356
- throw new Error(
1357
- 'Keyframes forbid nested rules and important declarations.',
1428
+ const frame = style(input)
1429
+ if (
1430
+ frame.rules ||
1431
+ frame.declarations.some((value) => value.important)
1358
1432
  )
1433
+ throw new Error(
1434
+ 'Keyframes forbid nested rules and important declarations.',
1435
+ )
1359
1436
 
1360
- return { stop, style: frame }
1361
- })
1362
-
1363
- if (call.exported || scanned.used.has(call.name!))
1364
- result.push({ kind: 'keyframes', name: call.name!, frames })
1365
- }
1366
- const contextValue = call.context ? value(call.context) : undefined
1367
- if (contextValue !== undefined) {
1368
- const context = record(contextValue)
1369
- if (Object.keys(context).some((key) => !['id', 'within'].includes(key)))
1370
- throw new Error('Unknown contribution context option.')
1371
- if (call.kind === 'customMedia' && 'within' in context)
1372
- throw new Error(
1373
- 'Custom media definitions do not accept a within context.',
1374
- )
1437
+ return { stop, style: frame }
1438
+ })
1375
1439
 
1376
- const within = context.within ?? []
1377
- if (
1378
- !Array.isArray(within) ||
1379
- within.some(
1380
- (header) =>
1381
- typeof header !== 'string' ||
1382
- !(
1383
- header === '@layer' ||
1384
- /^@(media|supports|container|layer)(?=[\t\n\r\f (/])/.test(
1385
- header,
1386
- )
1387
- ),
1388
- )
1389
- )
1390
- throw new Error('Expected enclosing conditional or layer headers.')
1391
- const headers = within.map((header: string) =>
1392
- Condition.normalize(header),
1393
- )
1394
- for (let index = before; index < result.length; index++)
1395
- result[index] = { ...result[index]!, within: headers }
1440
+ if (call.exported || scanned.used.has(call.name!))
1441
+ result.push({ kind: 'keyframes', name: call.name!, frames })
1442
+ }
1443
+ for (let index = before; index < result.length; index++) {
1444
+ if (headers.length)
1445
+ result[index] = { ...result[index]!, within: headers }
1446
+ starts?.push(call.start)
1447
+ }
1396
1448
  }
1397
-
1398
- for (let index = before; index < result.length; index++)
1399
- starts?.push(call.start)
1400
1449
  } catch (error) {
1401
1450
  throw new Themes.InvalidError((error as Error).message, call)
1402
1451
  }