Skip to content

Commit 2074a53

Browse files
committed
Update release workflow and add new features
- Refactor GitHub Actions workflows for simpler test and release processes. - Introduce new features like JSON language support, collapsible sections, button bindings, download feature, and improved gutter rail behavior.
1 parent 9bc06fa commit 2074a53

10 files changed

Lines changed: 913 additions & 62 deletions

.github/workflows/main.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ jobs:
2525
- name: Install dependencies
2626
run: npm install
2727

28-
- name: Install Playwright browsers
29-
run: npx playwright install chromium
30-
31-
- name: Run tests with coverage
28+
- name: Run tests
3229
run: npm test
3330
env:
3431
CI: true

.github/workflows/release.yml

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
name: Create Tag/Release
2-
run-name: ${{ github.actor }} run build on ${{ github.repository }}
2+
run-name: ${{ github.actor }} release ${{ inputs.bump }} on ${{ github.repository }}
33

44
on:
55
workflow_dispatch:
6+
inputs:
7+
bump:
8+
description: "Version bump type"
9+
required: true
10+
default: patch
11+
type: choice
12+
options:
13+
- patch
14+
- minor
15+
- major
16+
17+
permissions:
18+
contents: write
619

720
jobs:
821
create-release:
@@ -13,25 +26,30 @@ jobs:
1326
uses: actions/checkout@v4
1427
with:
1528
token: ${{ secrets.PAT_TOKEN }} # Necessary to trigger tag workflow
29+
fetch-depth: 0 # Full history + tags, needed by release-flow to compute the version
1630

1731
- name: Setup Node.js
1832
uses: actions/setup-node@v4
1933
with:
20-
node-version: '20'
34+
node-version: "20"
35+
- name: Install dependencies
36+
run: npm install
2137

22-
- name: Configure Git
23-
run: |
24-
git config user.name "$GITHUB_ACTOR"
25-
git config user.email ""
38+
- name: Run tests
39+
run: npm test
40+
env:
41+
CI: true
42+
- name: Build package
43+
run: npm run build
2644

27-
- name: Bump version (patch)
45+
- name: Check build output
2846
run: |
29-
npm version patch
30-
CURRENT_VERSION=$(node -p "require('./package.json').version")
31-
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
32-
NEXT_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
33-
sed -i "s/^# Release Notes$/# Release Notes\n\n## $NEXT_VERSION\n\n---/" RELEASE_NOTES.md
34-
git add RELEASE_NOTES.md
35-
git commit --amend --no-edit
36-
git push --all
37-
git push --tags
47+
if [ ! -f "dist/interactive-code.js" ]; then
48+
echo "Build failed: dist/interactive-code.js not found"
49+
exit 1
50+
fi
51+
echo "Build successful!"
52+
53+
- uses: softwarity/release-flow@v1
54+
with:
55+
bump: ${{ inputs.bump }} # the value picked in the dropdown

RELEASE_NOTES.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
# Release Notes
22

3-
## 1.0.8
3+
## NEXT RELEASE
4+
5+
### Features
6+
7+
- **JSON language**: New `language="json"` with dedicated syntax highlighting (property keys vs string values, `true`/`false`/`null`, numbers, punctuation). JSONC `//` and `/* */` comments are displayed but stripped from copy/download so the exported content stays valid (RFC 8259).
8+
- **Collapsible sections**: New `collapsible` attribute on `<textarea>` folds a range of non-interactive lines (GitHub-diff style), with `collapsed` to start folded. The collapsed band shows the hidden line count (`▸ ⋯ N lines`); when expanded, a collapse chevron sits in the gutter/margin of the first and last line. Folding is purely visual — copy/download still export the full content. Keyboard accessible (`role="button"`, Enter/Space).
9+
- **Button binding type**: New `type="button"` renders a clickable action token that emits a `change` event on every click (`e.detail` = its `value`) with no re-render — ideal for a hub of actions. `value` is the label (a `button<index>` default is synthesized when omitted). Exposes a `trigger()` method.
10+
- **Download button**: New `show-download` attribute displays a download button (next to copy) that exports the full content as a file. The file name comes from the optional `download="name.ext"` attribute or defaults to `snippet.<ext>` based on the language; MIME type matches the language.
11+
- **Gutter rail**: A fixed-width left gutter is now reserved whenever the component has line numbers, collapsible sections, or comment toggles — so fold chevrons and comment toggles share a consistent rail. Comment and block-comment toggles moved into the gutter, which **preserves the code indentation** regardless of comment state (toggling a comment no longer shifts the code). Width is customizable via `--code-gutter-width`.
12+
13+
### Improvements
14+
15+
- **Comment toggles no longer shift code**: line/block comment indicators are rendered in the gutter (out of flow), so commenting/uncommenting keeps the code aligned. Copy/download output is unchanged.
16+
17+
### Tests
18+
19+
- Added `src/new-features.spec.ts` (13 tests): JSON highlighting, JSONC comment stripping on export, collapsible sections, button type, download, and gutter behavior.
20+
- Fixed a pre-existing XSS test whose assertion checked `innerHTML` serialization (which legitimately decodes `&lt;` inside attribute values); it now asserts no `<script>` element is injected.
421

522
---
623

demo/index.html

Lines changed: 177 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,8 @@ <h1>@softwarity/interactive-code</h1>
489489
<!-- Description -->
490490
<p class="description">
491491
Web Component for displaying syntax-highlighted code with interactive bindings.
492-
Supports HTML, SCSS, TypeScript, and Shell with click-to-edit values.
492+
Supports HTML, SCSS, TypeScript, Shell, and JSON with click-to-edit values,
493+
collapsible sections, and copy/download.
493494
</p>
494495

495496
<!-- Theme & Color Scheme -->
@@ -991,6 +992,139 @@ <h3>Conditional Textareas</h3>
991992
</div>
992993
</div>
993994
</article>
995+
996+
<!-- JSON Type -->
997+
<article class="doc-section">
998+
<h3>JSON Type</h3>
999+
<p>Use <code>language="json"</code> for syntax-highlighted, editable JSON. JSONC <code>//</code> comments are shown but stripped from copy/download so the exported file stays valid. Add <code>show-download</code> (and optionally <code>download="name.json"</code>) for large payloads:</p>
1000+
1001+
<div class="code-header"><span>Written by developer</span></div>
1002+
<interactive-code class="inline-code" language="html" show-copy>
1003+
<textarea><interactive-code language="json" ${show-line-numbers} ${show-copy} ${show-download}>
1004+
<textarea>{
1005+
// edited live — this comment is stripped on copy/download
1006+
"theme": "${theme}",
1007+
"fontSize": ${fontSize},
1008+
"notifications": ${notifications}
1009+
}&lt;/textarea>
1010+
<code-binding key="theme" type="select" options="light,dark,auto" value="dark"></code-binding>
1011+
<code-binding key="fontSize" type="number" value="14" min="10" max="24"></code-binding>
1012+
<code-binding key="notifications" type="boolean" value="true"></code-binding>
1013+
</interactive-code></textarea>
1014+
<code-binding key="show-line-numbers" type="attribute" value="true"
1015+
onchange="var el=this.closest('.doc-section').querySelector('.user-view interactive-code'); e.detail ? el.setAttribute('show-line-numbers','') : el.removeAttribute('show-line-numbers')"></code-binding>
1016+
<code-binding key="show-copy" type="attribute" value="true"
1017+
onchange="var el=this.closest('.doc-section').querySelector('.user-view interactive-code'); e.detail ? el.setAttribute('show-copy','') : el.removeAttribute('show-copy')"></code-binding>
1018+
<code-binding key="show-download" type="attribute" value="true"
1019+
onchange="var el=this.closest('.doc-section').querySelector('.user-view interactive-code'); e.detail ? el.setAttribute('show-download','') : el.removeAttribute('show-download')"></code-binding>
1020+
</interactive-code>
1021+
1022+
<div class="preview-wrapper">
1023+
<div class="preview-label">Seen by user (edit, then try copy / download)</div>
1024+
<div class="user-view">
1025+
<interactive-code language="json" show-line-numbers show-copy show-download download="settings.json">
1026+
<textarea>{
1027+
// edited live — this comment is stripped on copy/download
1028+
"theme": "${theme}",
1029+
"fontSize": ${fontSize},
1030+
"notifications": ${notifications}
1031+
}</textarea>
1032+
<code-binding key="theme" type="select" options="light,dark,auto" value="dark"
1033+
onchange="applyJsonSettings(this)"></code-binding>
1034+
<code-binding key="fontSize" type="number" value="14" min="10" max="24"
1035+
onchange="applyJsonSettings(this)"></code-binding>
1036+
<code-binding key="notifications" type="boolean" value="true"
1037+
onchange="applyJsonSettings(this)"></code-binding>
1038+
</interactive-code>
1039+
<div class="live-element">
1040+
<div id="preview-json" class="preview-card" style="border-color: var(--accent-blue);">theme: dark &middot; 14px &middot; &#128276; on</div>
1041+
</div>
1042+
</div>
1043+
</div>
1044+
</article>
1045+
1046+
<!-- Collapsible Sections -->
1047+
<article class="doc-section">
1048+
<h3>Collapsible Sections</h3>
1049+
<p>Add <code>collapsible</code> on a <code>&lt;textarea&gt;</code> to fold a range of non-interactive lines (GitHub-diff style). Add <code>collapsed</code> to start folded. Folding is purely visual — copy/download still export the full content:</p>
1050+
1051+
<div class="code-header"><span>Written by developer</span></div>
1052+
<interactive-code class="inline-code" language="html" show-copy>
1053+
<textarea><interactive-code language="json" ${show-line-numbers} ${show-copy}>
1054+
<textarea>{
1055+
"id": "${id}",&lt;/textarea>
1056+
<textarea collapsible collapsed> "_internal": {
1057+
"trace": true,
1058+
"verbose": false,
1059+
"buffer": 4096
1060+
},&lt;/textarea>
1061+
<textarea> "enabled": ${enabled}
1062+
}&lt;/textarea>
1063+
<code-binding key="id" type="string" value="api"></code-binding>
1064+
<code-binding key="enabled" type="boolean" value="true"></code-binding>
1065+
</interactive-code></textarea>
1066+
<code-binding key="show-line-numbers" type="attribute" value="true"
1067+
onchange="var el=this.closest('.doc-section').querySelector('.user-view interactive-code'); e.detail ? el.setAttribute('show-line-numbers','') : el.removeAttribute('show-line-numbers')"></code-binding>
1068+
<code-binding key="show-copy" type="attribute" value="true"
1069+
onchange="var el=this.closest('.doc-section').querySelector('.user-view interactive-code'); e.detail ? el.setAttribute('show-copy','') : el.removeAttribute('show-copy')"></code-binding>
1070+
</interactive-code>
1071+
1072+
<div class="preview-wrapper">
1073+
<div class="preview-label">Seen by user (click the band to expand, copy to get the full JSON)</div>
1074+
<div class="user-view">
1075+
<interactive-code language="json" show-line-numbers show-copy>
1076+
<textarea>{
1077+
"id": "${id}",</textarea>
1078+
<textarea collapsible collapsed> "_internal": {
1079+
"trace": true,
1080+
"verbose": false,
1081+
"buffer": 4096
1082+
},</textarea>
1083+
<textarea> "enabled": ${enabled}
1084+
}</textarea>
1085+
<code-binding key="id" type="string" value="api"
1086+
onchange="updateFoldPreview(this)"></code-binding>
1087+
<code-binding key="enabled" type="boolean" value="true"
1088+
onchange="updateFoldPreview(this)"></code-binding>
1089+
</interactive-code>
1090+
<div class="live-element">
1091+
<div id="preview-fold" class="preview-card" style="border-color: var(--accent-purple);">id: api &middot; enabled: true</div>
1092+
</div>
1093+
</div>
1094+
</div>
1095+
</article>
1096+
1097+
<!-- Button Type -->
1098+
<article class="doc-section">
1099+
<h3>Button Type</h3>
1100+
<p>A <code>button</code> binding is a clickable action token: it emits a <code>change</code> event on every click (<code>e.detail</code> = its <code>value</code>), with no re-render. Great for a hub of actions:</p>
1101+
1102+
<div class="code-header"><span>Written by developer</span></div>
1103+
<interactive-code class="inline-code" language="html" show-copy>
1104+
<textarea><interactive-code language="typescript">
1105+
<textarea>await provider.${load}();
1106+
await provider.${save}();&lt;/textarea>
1107+
<code-binding key="load" type="button" value="load()"
1108+
onchange="alert(e.detail)"></code-binding>
1109+
<code-binding key="save" type="button" value="save()"
1110+
onchange="alert(e.detail)"></code-binding>
1111+
</interactive-code></textarea>
1112+
</interactive-code>
1113+
1114+
<div class="preview-wrapper">
1115+
<div class="preview-label">Seen by user (click an action)</div>
1116+
<div class="user-view">
1117+
<interactive-code language="typescript">
1118+
<textarea>await provider.${load}();
1119+
await provider.${save}();</textarea>
1120+
<code-binding key="load" type="button" value="load()"
1121+
onchange="alert(e.detail)"></code-binding>
1122+
<code-binding key="save" type="button" value="save()"
1123+
onchange="alert(e.detail)"></code-binding>
1124+
</interactive-code>
1125+
</div>
1126+
</div>
1127+
</article>
9941128
</section>
9951129

9961130
<!-- Documentation Section -->
@@ -1132,7 +1266,7 @@ <h3>API - &lt;interactive-code&gt;</h3>
11321266
<tbody>
11331267
<tr>
11341268
<td><code>language</code></td>
1135-
<td><code>'html' | 'scss' | 'typescript' | 'shell'</code></td>
1269+
<td><code>'html' | 'scss' | 'typescript' | 'shell' | 'json'</code></td>
11361270
<td><code>'html'</code></td>
11371271
<td>Syntax highlighting language</td>
11381272
</tr>
@@ -1154,6 +1288,18 @@ <h3>API - &lt;interactive-code&gt;</h3>
11541288
<td><code>false</code></td>
11551289
<td>Show the copy-to-clipboard button</td>
11561290
</tr>
1291+
<tr>
1292+
<td><code>show-download</code></td>
1293+
<td><code>boolean</code></td>
1294+
<td><code>false</code></td>
1295+
<td>Show the download button (exports the full content as a file)</td>
1296+
</tr>
1297+
<tr>
1298+
<td><code>download</code></td>
1299+
<td><code>string</code></td>
1300+
<td><code>snippet.&lt;ext&gt;</code></td>
1301+
<td>File name used by the download button</td>
1302+
</tr>
11571303
<tr>
11581304
<td><code>color-scheme</code></td>
11591305
<td><code>'light' | 'dark'</code></td>
@@ -1190,6 +1336,8 @@ <h4>Content</h4>
11901336
<ul class="feature-list">
11911337
<li><code>condition="key"</code> - Show when binding value is truthy</li>
11921338
<li><code>condition="!key"</code> - Show when binding value is falsy</li>
1339+
<li><code>collapsible</code> - Make the section foldable (band shows the hidden line count)</li>
1340+
<li><code>collapsed</code> - Start the section folded (requires <code>collapsible</code>)</li>
11931341
</ul>
11941342
</article>
11951343

@@ -1335,6 +1483,11 @@ <h3>Binding Types</h3>
13351483
<td>Line toggle</td>
13361484
<td>Checkbox to comment/uncomment line</td>
13371485
</tr>
1486+
<tr>
1487+
<td><code>button</code></td>
1488+
<td>Action token (<code>value</code> = label)</td>
1489+
<td>Click to fire a <code>change</code> event (<code>e.detail</code> = value)</td>
1490+
</tr>
13381491
<tr>
13391492
<td><code>readonly</code></td>
13401493
<td>Display only</td>
@@ -1349,9 +1502,11 @@ <h3>Binding Types</h3>
13491502
<article class="doc-section">
13501503
<h3>Features</h3>
13511504
<ul class="feature-list">
1352-
<li><strong>Syntax Highlighting</strong> - HTML, SCSS, TypeScript, Shell</li>
1505+
<li><strong>Syntax Highlighting</strong> - HTML, SCSS, TypeScript, Shell, JSON</li>
13531506
<li><strong>Interactive Bindings</strong> - Click to edit values directly in the code</li>
1354-
<li><strong>Multiple Types</strong> - boolean, number, string, select, color, attribute, comment</li>
1507+
<li><strong>Multiple Types</strong> - boolean, number, string, select, color, attribute, comment, button</li>
1508+
<li><strong>Collapsible Sections</strong> - Fold non-interactive ranges; copy/download stay complete</li>
1509+
<li><strong>Copy &amp; Download</strong> - <code>show-copy</code> / <code>show-download</code> buttons (valid JSON export)</li>
13551510
<li><strong>Framework Agnostic</strong> - Works with Angular, React, Vue, or vanilla JS</li>
13561511
<li><strong>Shadow DOM</strong> - Encapsulated styles</li>
13571512
<li><strong>Zero Dependencies</strong> - Pure Web Components</li>
@@ -1385,6 +1540,24 @@ <h3>Features</h3>
13851540
// Load initial theme (catppuccin)
13861541
loadThemeCSS('catppuccin');
13871542

1543+
// JSON demo: reflect the edited settings into the live element
1544+
function applyJsonSettings(binding) {
1545+
var ic = binding.closest('interactive-code');
1546+
var theme = ic.querySelector('code-binding[key=theme]').value;
1547+
var fontSize = ic.querySelector('code-binding[key=fontSize]').value;
1548+
var notif = String(ic.querySelector('code-binding[key=notifications]').value) !== 'false';
1549+
document.getElementById('preview-json').textContent =
1550+
'theme: ' + theme + ' · ' + fontSize + 'px · ' + (notif ? '🔔 on' : '🔕 off');
1551+
}
1552+
1553+
// Collapsible demo: reflect id/enabled into the live element
1554+
function updateFoldPreview(binding) {
1555+
var ic = binding.closest('interactive-code');
1556+
var id = ic.querySelector('code-binding[key=id]').value;
1557+
var enabled = ic.querySelector('code-binding[key=enabled]').value;
1558+
document.getElementById('preview-fold').textContent = 'id: ' + id + ' · enabled: ' + enabled;
1559+
}
1560+
13881561
// Apply initial interactive style (hand-drawn)
13891562
applyInteractiveStyle('hand-drawn');
13901563

src/code-binding.element.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type BindingType = 'boolean' | 'number' | 'string' | 'select' | 'color' | 'comment' | 'attribute' | 'readonly';
1+
export type BindingType = 'boolean' | 'number' | 'string' | 'select' | 'color' | 'comment' | 'attribute' | 'readonly' | 'button';
22

33
/**
44
* <code-binding> Web Component
@@ -172,4 +172,27 @@ export class CodeBindingElement extends HTMLElement {
172172
this.value = newValue;
173173
}
174174
}
175+
176+
/**
177+
* Trigger a button action.
178+
* Unlike value setters, this emits `change` on every activation (no value comparison),
179+
* since a button has no value to change — it just fires the action.
180+
*/
181+
trigger() {
182+
if (this._disabled) return;
183+
if (this.type === 'button') {
184+
this.emitChange();
185+
}
186+
}
187+
188+
/**
189+
* Set an initial value without emitting a `change` event.
190+
* Used to synthesize a default label (e.g. `button0`) for `button` bindings
191+
* that omit the `value` attribute.
192+
*/
193+
setDefaultValue(v: any) {
194+
if (this._value === undefined || this._value === null || this._value === '') {
195+
this._value = this.parseValue(v);
196+
}
197+
}
175198
}

src/interactive-code.element.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -889,9 +889,12 @@ describe('InteractiveCodeElement', () => {
889889
await new Promise(resolve => setTimeout(resolve, 150));
890890

891891
const code = element.shadowRoot?.querySelector('code');
892-
// Should not contain unescaped script tag
893-
expect(code?.innerHTML).not.toContain('<script>');
894-
// Should contain escaped version
892+
// No executable <script> element must be injected into the rendered DOM.
893+
// (Checking innerHTML for the "<script>" substring is unreliable: a *value*
894+
// attribute legitimately serializes "&lt;" back to "<" per the HTML spec,
895+
// even though the value is inert. Asserting on the actual DOM is the real check.)
896+
expect(code?.querySelector('script')).toBeNull();
897+
// The displayed value text is escaped
895898
expect(code?.innerHTML).toContain('&lt;script&gt;');
896899
});
897900

0 commit comments

Comments
 (0)