Skip to content

Commit 7fa7957

Browse files
feat(template): add customizable code generation template system (#249)
* docs(templates): document template system and color mapping in ARCHITECTURE.md * feat(template): add core template engine with lexer, resolver, and config parser * feat(gradle-plugin): add templateFile option and functional tests * chore: bump version and add template integrity check CI * docs: add template system to CLI, Gradle plugin READMEs, and help output * feat(playground): add template editor with validation, autocomplete, and persistence * docs(website): add template system documentation page * chore: remove conditional check for nodeJS environment setup in pull request workflow * fix(ci): use explicit bash invocation and numeric comparison in template script - Invoke template-script.sh via `bash` for portability when executable permissions aren't preserved. - Use `-eq` instead of `==` for numeric array-length comparison. * fix(template): consume entire malformed placeholder span as literal in lexer When tryParsePlaceholder returns null for a `${...}` sequence, consume the entire span up to the closing `}` as literal text instead of just the `$` character. This prevents inner content from being re-interpreted as nested placeholders. * fix(template): fall through to default preview when template key is null - When [templates.preview] exists but the template key is null (e.g. empty section), fall through to default preview behavior instead of suppressing preview generation. - Refactor buildPreview to use a when expression. * fix(template): use template-aware emission for group fallback and chunk functions - Extract emitGroupHeader from ImageVectorNodeEmitter so the fallback path in TemplateNodeEmitter still recurses children template-aware. - Make splitTopLevelParams and findMatchingCloseParen respect string literals, angle brackets, braces, and brackets via a shared NestingTracker. * fix(template): include fileHeader in definition key validation Add config.templates.fileHeader to allTemplateTexts so that ${def:...} references in file headers are validated against defined import keys. * docs: fix templateFile() behavior inconsistencies and chunk:index comment - Align docs, website, and playground on consistent templateFile() behavior: omitting it in Gradle disables auto-discovery; use common config for inheritance. - Fix method signature to templateFile(path: RegularFile). Correct chunk:index comment from 0-based to 1-based. * fix(template): enhance flag parsing and default differ handling in script * refactor(ci): use flag-based args and add --default-differ to integrity scripts Refactor both integrity check scripts to parse flags via a while/case loop instead of positional arguments. Add --default-differ flag to skip delta and use plain diff for machine-readable output. * feat(template): add emitClipPathData function and integrate clip path handling in group parameters * feat(template): add iconGroup function for enhanced group handling in ImageVector * feat(template): replace group with iconGroup for improved clip path handling in SVG templates * fix: bad rebase state * feat(navigation): implement WebRoute structure for improved routing management * feat(website): add CodeAwareSpanText component with callout variants Replaces RichSpanText with a more descriptive name. Renders text that may contain inline code segments using backtick syntax. Adds typed CssStyleVariant support with per-CalloutVariant variants (Tip, Warning, Important) for proper InlineCode colour theming inside callouts. * refactor(website): convert docs to CodeAwareSpanText and fix Canadian English Replace Span { Text() InlineCode() } patterns across all five docs files with CodeAwareSpanText using backtick syntax. Callout blocks now use typed variants instead of manually resolving InlineCodeVars. Fix US English spellings in copy text: color to colour in section titles, descriptions, and variable labels. Code identifiers (Color, SolidColor, color_mapping) are left unchanged. * docs(website): add KDoc to WebRoute and CodeAwareSpanText Add KDoc to WebRoute class, its properties, and companion entries. Add KDoc to CodeAwareSpanText composable, ComponentKind marker, and all three callout style variants. * fix(website): correct CodeAwareSpanText parsing when text starts with backtick The previous logic dropped leading empty parts and reset the index, which inverted the even/odd check and rendered code as text and vice versa. Now iterates all parts using the original split index and skips empty segments instead. * fix(website): replace "elision/elides" with "trimming/trims" in copy and tests Update ToC entry in Templates.kt and test names in PlaceholderResolverTest.kt to use "trim" instead of "elide". * refactor(website): convert remaining InlineCode patterns to CodeAwareSpanText Convert TemplateSystemCallout in CliDocsContent and --max-workers Li in GradlePluginDocsContent to use CodeAwareSpanText with backtick syntax. Remove now-unused resolveInlineCodeColors from DocsStyles. * fix(website): use Canadian English spelling for "customise/customisation" Replace all US English "customize/customization/customizing" with "customise/customisation/customising" in website copy across docs, playground, and landing page. * refactor(website): replace SpanText with CodeAwareSpanText and update filename casing in documentation * fix: address code review findings across script, tests, and website - fix(ci): clean up cli-integrity-check script parameter parsing, remove vestigial $2 assignment, add --extension required validation, fix suffix not being set to "optimized" when --optimize flag is passed, use POSIX = for string comparisons and -eq for numeric comparisons - fix(gradle-plugin): replace auto-bootstrap with assertion failure for missing template golden files in functional tests - fix(website): remove trailing spaces after line-continuation backslashes in FaqContent shell code block - fix(website): expand icon namespace scope to include file_header and preview contexts in TemplateDocsContent - fix(website): remove editorial inline comment from null-handling code example in TemplateDocsContent - fix(website): correct contradictory auto-discovery guidance in TemplateDocsContent - omitting templateFile() inherits common config - refactor(website): use lazy delegate for WebRoute.all property * feat: integrate template configuration support in Processor * feat: add template option for customizable Kotlin code generation * feat: add option to disable template auto-discovery in Kotlin code generation
1 parent 1d3512a commit 7fa7957

File tree

128 files changed

+14630
-2087
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+14630
-2087
lines changed

.github/actions/cli-integrity-check/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ runs:
1111
- name: Verify ${{ inputs.type }} conversion without optimization
1212
shell: bash
1313
run: |
14-
./.github/actions/cli-integrity-check/script.sh . ${{ inputs.type }}
14+
./.github/actions/cli-integrity-check/script.sh . --extension ${{ inputs.type }}
1515
- name: Verify ${{ inputs.type }} conversion with optimization
1616
shell: bash
1717
run: |
18-
./.github/actions/cli-integrity-check/script.sh . ${{ inputs.type }} optimize
18+
./.github/actions/cli-integrity-check/script.sh . --extension ${{ inputs.type }} --optimize

.github/actions/cli-integrity-check/script.sh

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,59 @@
11
#!/bin/bash
2-
# Parse and validate required parameters
2+
# Parse and validate the required positional parameter
33
root_directory="${1:?Error: Root directory must be the first parameter}"
4-
ext="${2:?Error: File extension must be the second parameter}"
4+
shift # consume root_directory, leaving only flags
55

6-
# Parse optimization and rebuild flags (rebuild can be $3 or $4)
6+
# Parse flags
77
optimize="false"
88
suffix="nonoptimized"
99
rebuild=""
10+
use_default_differ="false"
11+
rebuild_applied="false"
12+
ext=""
13+
14+
while [ $# -gt 0 ]; do
15+
case "$1" in
16+
--extension)
17+
ext="${2:?Error: --extension flag requires a value}"
18+
shift 2
19+
;;
20+
--optimize)
21+
optimize="true"
22+
suffix="optimized"
23+
shift
24+
;;
25+
--rebuild)
26+
rebuild="--upgrade"
27+
shift
28+
;;
29+
--default-differ)
30+
use_default_differ="true"
31+
shift
32+
;;
33+
*)
34+
echo "Warning: unknown flag '$1'" >&2
35+
shift
36+
;;
37+
esac
38+
done
1039

11-
if [ "$3" == "optimize" ]; then
12-
optimize="true"
13-
suffix="optimized"
14-
[ "$4" == "--rebuild" ] && rebuild="--upgrade"
15-
elif [ "$3" == "--rebuild" ]; then
16-
rebuild="--upgrade"
40+
if [ -z "$ext" ]; then
41+
echo "Error: --extension flag is required" >&2
42+
exit 1
1743
fi
1844

1945
# Determine file type based on extension
20-
if [ "$ext" == "xml" ]; then
46+
if [ "$ext" = "xml" ]; then
2147
type="avg"
2248
else
2349
type="svg"
2450
ext="svg"
2551
fi
2652

27-
rebuild_applied="false"
53+
differ="diff --strip-trailing-cr"
54+
if [ "$use_default_differ" = "false" ] && command -v delta &> /dev/null; then
55+
differ="delta -s --paging=never"
56+
fi
2857

2958
# Package must match exactly what the Gradle plugin functional tests use so
3059
# both tools validate against the same expected .kt files.
@@ -34,10 +63,10 @@ errors=()
3463

3564
# Convert kebab-case or snake_case filename to PascalCase.
3665
to_pascal_case() {
37-
echo "$1" | awk -F'[-_]' '{ for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2); print }' OFS=''
38-
}
66+
echo "$1" | awk -F'[-_]' '{ for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2); print }' OFS=''
67+
}
3968

40-
# Only process flat (non-directory) files in samples/{type}/ subdirectories
69+
# Only process flat (non-directory) files in samples/{type}/ - subdirectories
4170
# such as gradient/, rects/, css/ etc. are intentionally excluded.
4271
for input in "$root_directory/samples/${type}"/*."${ext}"; do
4372
[ -f "$input" ] || continue
@@ -47,8 +76,8 @@ for input in "$root_directory/samples/${type}"/*."${ext}"; do
4776
icon_name=$(to_pascal_case "$stem")
4877
expected_file="$expected_dir/${icon_name}.${ext}.${suffix}.kt"
4978

50-
if [ "$optimize" == "true" ]; then
51-
if [ "$type" == "svg" ]; then
79+
if [ "$optimize" = "true" ]; then
80+
if [ "$type" = "svg" ]; then
5281
echo "svgo version $(svgo --version)"
5382
else
5483
echo "avocado version $(avocado --version)"
@@ -64,7 +93,7 @@ for input in "$root_directory/samples/${type}"/*."${ext}"; do
6493
tmp_output="${tmp_dir}/${icon_name}.kt"
6594

6695
rebuild_arg=""
67-
if [ "$rebuild_applied" == "false" ]; then
96+
if [ "$rebuild_applied" = "false" ]; then
6897
rebuild_arg="$rebuild"
6998
fi
7099

@@ -96,7 +125,7 @@ for input in "$root_directory/samples/${type}"/*."${ext}"; do
96125
fi
97126

98127
echo "Verifying $stem against expected file."
99-
if ! diff --strip-trailing-cr "$tmp_output" "$expected_file"; then
128+
if ! $differ "$tmp_output" "$expected_file"; then
100129
errors+=("$stem.$ext")
101130
else
102131
echo "$stem.$ext pass"
@@ -106,7 +135,7 @@ done
106135

107136
echo
108137
echo
109-
if [ "${#errors[@]}" == 0 ]; then
138+
if [ "${#errors[@]}" -eq 0 ]; then
110139
echo "✅ Integrity check pass"
111140
exit 0
112141
else
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: Template Integrity check
2+
description: Runs the CLI command checking if template-based output still works as intended
3+
runs:
4+
using: composite
5+
steps:
6+
- name: Verify template conversion
7+
shell: bash
8+
run: |
9+
bash ./.github/actions/template-integrity-check/script.sh .
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/bin/bash
2+
root_directory=$1
3+
if [ "$root_directory" == "" ]; then
4+
echo "The root directory must be the first parameter."
5+
exit 1
6+
fi
7+
8+
use_default_differ="false"
9+
rebuild=""
10+
11+
shift # consume root_directory ($1), leaving only flags
12+
while [ $# -gt 0 ]; do
13+
case "$1" in
14+
--rebuild)
15+
rebuild="--upgrade"
16+
shift
17+
;;
18+
--default-differ)
19+
use_default_differ="true"
20+
shift
21+
;;
22+
*)
23+
echo "Warning: unknown flag '$1'" >&2
24+
shift
25+
;;
26+
esac
27+
done
28+
29+
differ="diff --strip-trailing-cr"
30+
if [ "$use_default_differ" == "false" ] && command -v delta &> /dev/null; then
31+
differ="delta -s --paging=never"
32+
fi
33+
rebuild_applied="false"
34+
35+
template_file="$root_directory/playground/s2c.template.toml"
36+
expected_dir="$root_directory/integrity-check/expected/template"
37+
errors=()
38+
39+
# Convert kebab-case or snake_case filename to PascalCase.
40+
to_pascal_case() {
41+
echo "$1" | awk -F'[-_]' '{ for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2); print }' OFS=''
42+
}
43+
44+
# Hardcoded list of input files covering all 5 categories (simple, groups,
45+
# chunks, gradients, masks) across SVG and AVG.
46+
svg_files=(
47+
"samples/svg/attention-filled.svg"
48+
"samples/svg/android.svg"
49+
"samples/svg/brasil.svg"
50+
"samples/svg/gradient/linear-gradient01.svg"
51+
"samples/svg/mask/mask-with-group.svg"
52+
)
53+
avg_files=(
54+
"samples/avg/shield-halved-solid.xml"
55+
"samples/avg/android.xml"
56+
"samples/avg/gradient/stroke_gradient.xml"
57+
)
58+
59+
process_file() {
60+
local input="$1"
61+
local type="$2"
62+
local ext="$3"
63+
local package="dev.tonholo.s2c.integrity.icon.${type}"
64+
65+
basename="${input##*/}"
66+
stem="${basename%.*}"
67+
icon_name=$(to_pascal_case "$stem")
68+
expected_file="$expected_dir/${icon_name}.${ext}.template.kt"
69+
70+
s2c_version=$(command "$root_directory/s2c" --version)
71+
echo "Parsing $stem to Jetpack Compose Icon (template) using $s2c_version"
72+
73+
tmp_dir="$(mktemp -d)"
74+
tmp_output="${tmp_dir}/${icon_name}.kt"
75+
76+
rebuild_arg=""
77+
if [ "$rebuild_applied" == "false" ]; then
78+
rebuild_arg="$rebuild"
79+
fi
80+
81+
if ! command "$root_directory/s2c" \
82+
-o "$tmp_output" \
83+
-p "$package" \
84+
--theme "" \
85+
--no-preview \
86+
--template "$template_file" \
87+
${rebuild_arg:+"$rebuild_arg"} \
88+
"$root_directory/$input"; then
89+
echo "Failed to execute CLI template integrity check for $stem."
90+
rm -rf "$tmp_dir"
91+
errors+=("$stem.$ext")
92+
return
93+
fi
94+
95+
if [ -n "$rebuild" ]; then
96+
rebuild_applied="true"
97+
fi
98+
99+
if [ ! -f "$expected_file" ]; then
100+
mkdir -p "$expected_dir"
101+
cp "$tmp_output" "$expected_file"
102+
echo "BOOTSTRAP: Wrote expected file ${icon_name}.${ext}.template.kt"
103+
rm -rf "$tmp_dir"
104+
return
105+
fi
106+
107+
echo "Verifying $stem (template) against expected file."
108+
if ! $differ "$tmp_output" "$expected_file"; then
109+
errors+=("$stem.$ext")
110+
else
111+
echo "$stem.$ext (template) pass"
112+
fi
113+
rm -rf "$tmp_dir"
114+
}
115+
116+
for input in "${svg_files[@]}"; do
117+
process_file "$input" "svg" "svg"
118+
done
119+
120+
for input in "${avg_files[@]}"; do
121+
process_file "$input" "avg" "xml"
122+
done
123+
124+
echo
125+
echo
126+
if [ "${#errors[@]}" -eq 0 ]; then
127+
echo "✅ Template integrity check pass"
128+
exit 0
129+
else
130+
echo "❌ Template integrity check failed"
131+
echo
132+
echo "Failed files:"
133+
for f in "${errors[@]}"; do
134+
echo " - $f"
135+
done
136+
exit 1
137+
fi

0 commit comments

Comments
 (0)