Skip to content

Commit e1d1990

Browse files
committed
feat: enhance code structure handling and UI interactions
- Added initialScript.js to provide a default script template for users. - Updated App.vue and CodeStructuresStage.vue to improve layout and visual feedback with new padding and highlight effects. - Introduced a new 'No Transform' option in the template catalog for exporting match-only scaffolds. - Enhanced store functionality with methods for adding pipeline steps and handling no-transform steps. - Improved ParseButton and PipelineBuilder components to support new features and better user experience.
1 parent 47f7455 commit e1d1990

10 files changed

Lines changed: 580 additions & 27 deletions

src/App.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ onBeforeUnmount(() => {
128128
min-width: 0;
129129
display: flex;
130130
align-items: center;
131+
padding-top: 0.35rem;
131132
}
132133
133134
.left-column {

src/components/CodeStructuresStage.vue

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ const hasResults = computed(() =>
1313
(store.areFiltersActive ? store.filteredNodes : store.arb?.ast ?? []).length > 0,
1414
);
1515
const hasNodeInfo = computed(() => Boolean(store.getSelectedNode()));
16+
const shouldHighlightResults = computed(() =>
17+
hasResults.value &&
18+
!store.hasVisitedExploreNodes &&
19+
!store.shouldPulseCodeStructuresStage,
20+
);
21+
const shouldPulseStructures = computed(() =>
22+
store.shouldPulseCodeStructuresStage,
23+
);
1624
1725
const tabs = computed(() => [
1826
{
@@ -78,6 +86,7 @@ function openTab(tabId) {
7886
return;
7987
}
8088
89+
store.shouldPulseCodeStructuresStage = false;
8190
store.setActiveWorkspaceTab('explorer');
8291
}
8392
</script>
@@ -89,7 +98,11 @@ function openTab(tabId) {
8998
v-for="tab in tabs"
9099
:key="tab.id"
91100
class="subtab-btn"
92-
:class="{active: activeSubview === tab.id}"
101+
:class="{
102+
active: activeSubview === tab.id,
103+
highlighted: tab.id === 'results' && shouldHighlightResults,
104+
pulsating: tab.id === 'structures' && shouldPulseStructures,
105+
}"
93106
type="button"
94107
:disabled="!tab.enabled || activeSubview === tab.id"
95108
:title="tab.label"
@@ -120,6 +133,8 @@ function openTab(tabId) {
120133
display: flex;
121134
gap: 0.55rem;
122135
flex-wrap: wrap;
136+
padding-top: 0.35rem;
137+
padding-left: 0.35rem;
123138
}
124139
125140
.subtab-btn {
@@ -141,6 +156,35 @@ function openTab(tabId) {
141156
box-shadow: inset 0 0 0 1px rgba(126, 202, 255, 0.12);
142157
}
143158
159+
.subtab-btn.highlighted {
160+
border-color: rgba(255, 215, 64, 0.95);
161+
background: rgba(255, 215, 64, 0.2);
162+
box-shadow: 0 0 0 0 rgba(255, 215, 64, 0.65);
163+
animation: pulse-results-glow 2s infinite;
164+
}
165+
166+
.subtab-btn.highlighted:hover:not(:disabled):not(.active),
167+
.subtab-btn.highlighted:focus-visible:not(:disabled):not(.active) {
168+
background: rgba(255, 215, 64, 0.26);
169+
border-color: rgba(255, 215, 64, 1);
170+
}
171+
172+
.subtab-btn.pulsating {
173+
border-color: rgba(255, 215, 64, 0.95);
174+
background: rgba(255, 215, 64, 0.22);
175+
box-shadow: 0 0 0 0 rgba(255, 215, 64, 0.7);
176+
animation: pulse-structures-glow 2s infinite;
177+
position: relative;
178+
z-index: 1;
179+
}
180+
181+
.subtab-btn.pulsating:hover:not(:disabled):not(.active),
182+
.subtab-btn.pulsating:focus-visible:not(:disabled):not(.active) {
183+
background: rgba(255, 215, 64, 0.28);
184+
border-color: rgba(255, 215, 64, 1);
185+
outline: none;
186+
}
187+
144188
.subtab-btn:hover:not(:disabled):not(.active),
145189
.subtab-btn:focus-visible:not(:disabled):not(.active) {
146190
background: rgba(126, 202, 255, 0.1);
@@ -162,4 +206,32 @@ function openTab(tabId) {
162206
width: 1rem;
163207
height: 1rem;
164208
}
209+
210+
@keyframes pulse-results-glow {
211+
0% {
212+
box-shadow:
213+
0 0 0 0 rgba(255, 215, 64, 0.65),
214+
0 0 18px rgba(255, 215, 64, 0.28);
215+
}
216+
217+
100% {
218+
box-shadow:
219+
0 0 0 15px rgba(255, 215, 64, 0),
220+
0 0 24px rgba(255, 215, 64, 0.12);
221+
}
222+
}
223+
224+
@keyframes pulse-structures-glow {
225+
0% {
226+
box-shadow:
227+
0 0 0 0 rgba(255, 215, 64, 0.7),
228+
0 0 9px rgba(255, 215, 64, 0.32);
229+
}
230+
231+
100% {
232+
box-shadow:
233+
0 0 0 7.5px rgba(255, 215, 64, 0),
234+
0 0 12px rgba(255, 215, 64, 0.14);
235+
}
236+
}
165237
</style>

src/components/FileLoader.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,13 @@ function loadSample(sampleId) {
208208
0% {
209209
box-shadow:
210210
0 0 0 0 rgba(0, 204, 255, 0.7),
211-
0 0 18px rgba(0, 204, 255, 0.32);
211+
0 0 9px rgba(0, 204, 255, 0.32);
212212
}
213213
214214
100% {
215215
box-shadow:
216-
0 0 0 15px rgba(0, 204, 255, 0),
217-
0 0 24px rgba(0, 204, 255, 0.14);
216+
0 0 0 7.5px rgba(0, 204, 255, 0),
217+
0 0 12px rgba(0, 204, 255, 0.14);
218218
}
219219
}
220220

src/components/InputCodeEditor.vue

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
11
<script setup>
2+
import initialScript from '../initialScript.js?raw';
23
import store from '../store';
34
import CodeEditor from './CodeEditor.vue';
45
5-
const initialValue = `/*
6-
flASTer usage:
7-
8-
1. Paste JavaScript here or load a local file / saved sample from the top menu.
9-
2. Samples parse automatically. For pasted code or loaded files, click Parse to build the AST and enable structure matching.
10-
3. Use the Structure Explorer to match patterns, inspect results, and open transforms.
11-
4. Apply built-in or custom transforms from the inspector when you are ready.
12-
13-
Replace this comment with the code you want to analyze.
14-
*/`;
15-
166
</script>
177

188
<template>
199
<div class="code-editor-wrapper">
20-
<code-editor :editor-id="store.editorIds.inputCodeEditor" :initial-value="initialValue"/>
10+
<code-editor :editor-id="store.editorIds.inputCodeEditor" :initial-value="initialScript"/>
2111
</div>
2212
</template>
2313

src/components/ParseButton.vue

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ function resetParsedState() {
4343
store.arb = {ast: [], script: ''};
4444
store.clearKnownStructureResults();
4545
store.parsedContentVersion = -1;
46+
store.hasVisitedExploreNodes = false;
47+
store.shouldPulseCodeStructuresStage = false;
4648
setContentUnparsed();
4749
store.page = 0;
4850
}
4951
50-
function parseContent() {
52+
function parseContent({focusExploreNodes = false, pulseCodeStructures = false} = {}) {
5153
if (!canParse.value) {
5254
return;
5355
}
@@ -73,6 +75,15 @@ function parseContent() {
7375
store.filteredNodes = store.arb.ast;
7476
store.markCurrentInputParsed();
7577
setContentParsed();
78+
79+
if (focusExploreNodes) {
80+
store.setActiveWorkspaceTab('results');
81+
store.setActiveInspectorPanel('browser');
82+
}
83+
84+
if (pulseCodeStructures) {
85+
store.shouldPulseCodeStructuresStage = true;
86+
}
7687
}).catch((error) => store.logMessage(error.message, 'error'));
7788
} catch (error) {
7889
store.logMessage(error.message, 'error');
@@ -88,7 +99,10 @@ onMounted(() => {
8899
}
89100
90101
store.shouldAutoParseInitialInput = false;
91-
parseContent();
102+
parseContent({
103+
focusExploreNodes: true,
104+
pulseCodeStructures: true,
105+
});
92106
return true;
93107
};
94108

src/components/PipelineBuilder.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import IconArrowUp from './icons/IconArrowUp.vue';
44
import IconArrowDown from './icons/IconArrowDown.vue';
55
import IconEye from './icons/IconEye.vue';
66
import IconClose from './icons/IconClose.vue';
7+
import IconTransform from './icons/IconTransform.vue';
78
</script>
89

910
<template>
@@ -52,6 +53,15 @@ import IconClose from './icons/IconClose.vue';
5253
<button class="mini-btn icon-btn icon-btn-sm" type="button" :title="step.enabled === false ? 'Enable this step' : 'Disable this step'" :aria-label="step.enabled === false ? 'Enable step' : 'Disable step'" @click.stop="store.togglePipelineStep(index)">
5354
<icon-eye />
5455
</button>
56+
<button
57+
class="mini-btn icon-btn icon-btn-sm"
58+
type="button"
59+
title="Replace this step by reparsing and reopening the Transform panel"
60+
aria-label="Replace step transform"
61+
@click.stop="store.editPipelineStep(index)"
62+
>
63+
<icon-transform />
64+
</button>
5565
<button class="mini-btn icon-btn icon-btn-sm" type="button" title="Remove this step from the pipeline" aria-label="Remove step" @click.stop="store.removePipelineStep(index)">
5666
<icon-close />
5767
</button>

src/components/TemplateWorkbench.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,16 @@ const transformOptions = computed(() => store.templateCatalog.map((template) =>
222222
};
223223
}
224224
225+
if (template.type === 'no-transform') {
226+
return {
227+
...template,
228+
disabled: !activeStructure.value || activeMatchCount.value < 1,
229+
detail: activeStructure.value && activeMatchCount.value > 0
230+
? 'Export matcher scaffolding without changing the current script.'
231+
: 'Choose a matched structure first.',
232+
};
233+
}
234+
225235
return {
226236
...template,
227237
disabled: !activeStructure.value || activeMatchCount.value < 1,

src/composition/scriptGenerator.js

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ function createImportPlan(steps) {
149149
});
150150

151151
if (implementation?.importPath && implementation?.namespaceImport) {
152-
needsKnownStructureRuntime = true;
152+
if (stepNeedsKnownStructureRuntime(step)) {
153+
needsKnownStructureRuntime = true;
154+
}
153155
const importEntry = restringerImports.get(implementation.importPath) ?? {
154156
importPath: implementation.importPath,
155157
namespaceImport: implementation.namespaceImport,
@@ -257,6 +259,10 @@ function createPipelineEpilogue() {
257259
* @returns {string}
258260
*/
259261
function createCustomStepBlock(step, stepNumber, combineFilters) {
262+
if (isNoTransformStep(step)) {
263+
return createNoTransformStepBlock(step, stepNumber, combineFilters);
264+
}
265+
260266
const enabledFilters = step?.kind === 'custom'
261267
? step.filters?.filter((filter) => filter?.enabled && !!filter?.src) ?? []
262268
: [];
@@ -429,6 +435,14 @@ function isStructureSelectionStep(step) {
429435
step?.templateType === 'isolate-structure-matches';
430436
}
431437

438+
function isNoTransformStep(step) {
439+
return step?.templateType === 'no-transform';
440+
}
441+
442+
function stepNeedsKnownStructureRuntime(step) {
443+
return step?.kind === 'known-structure-transform' || isStructureSelectionStep(step);
444+
}
445+
432446
function getCustomStepRunMode(step) {
433447
const runMode = step?.runMode ?? step?.params?.runMode ?? 'until-stable';
434448
return ['once', 'count', 'until-stable'].includes(runMode) ? runMode : 'until-stable';
@@ -449,6 +463,79 @@ function getCustomStepMaxIterations(step) {
449463
return 1;
450464
}
451465

466+
function createNoTransformStepBlock(step, stepNumber, combineFilters) {
467+
const structureId = step?.selectionSource?.kind === 'known-structure'
468+
? step.selectionSource.structureId ?? step?.params?.structureId ?? ''
469+
: step?.params?.structureId ?? '';
470+
const implementation = structureId
471+
? maybeResolveKnownStructureImplementation({structureId})
472+
: null;
473+
const enabledFilters = step?.kind === 'custom'
474+
? step.filters?.filter((filter) => filter?.enabled && !!filter?.src) ?? []
475+
: [];
476+
const filter = enabledFilters.length
477+
? combineFilters(enabledFilters.map((filterEntry) => filterEntry.src))
478+
: 'true';
479+
const arbVar = `arb${stepNumber}`;
480+
const rawMatchesVar = `rawMatches${stepNumber}`;
481+
const matchFuncVar = `customMatchFunc${stepNumber}`;
482+
const transformFuncVar = `customTransform${stepNumber}`;
483+
const nextChangesVar = `appliedChanges${stepNumber}`;
484+
485+
if (implementation?.matcherName && implementation?.namespaceImport) {
486+
return `// Step ${stepNumber}: ${step?.label ?? 'No Transform'}
487+
function ${transformFuncVar}(arb, matches) {
488+
// Intentionally empty. Edit this after export.
489+
return arb;
490+
}
491+
let ${arbVar} = new Arborist(script);
492+
const ${rawMatchesVar} = ${implementation.namespaceImport}.${implementation.matcherName}(${arbVar}, () => true);
493+
494+
${transformFuncVar}(${arbVar}, ${rawMatchesVar});
495+
496+
const ${nextChangesVar} = ${arbVar}.applyChanges();
497+
script = ${arbVar}.script;
498+
499+
console.debug(
500+
\`[i] Step ${stepNumber} matched \${${rawMatchesVar}.length} group\${${rawMatchesVar}.length === 1 ? '' : 's'} for ${step?.label ?? 'No Transform'}\`,
501+
);
502+
503+
if (${nextChangesVar} > 0) {
504+
console.debug(
505+
\`[+] Step ${stepNumber} applied \${${nextChangesVar}} change\${${nextChangesVar} === 1 ? '' : 's'}\`,
506+
);
507+
} else {
508+
console.debug(\`[!] Step ${stepNumber} did not change the script\`);
509+
}`;
510+
}
511+
512+
return `// Step ${stepNumber}: ${step?.label ?? 'No Transform'}
513+
const ${matchFuncVar} = (arb) => (arb.ast ?? []).filter((n) => ${filter});
514+
function ${transformFuncVar}(arb, matches) {
515+
// Intentionally empty. Edit this after export.
516+
return arb;
517+
}
518+
let ${arbVar} = new Arborist(script);
519+
const ${rawMatchesVar} = ${matchFuncVar}(${arbVar});
520+
521+
${transformFuncVar}(${arbVar}, ${rawMatchesVar});
522+
523+
const ${nextChangesVar} = ${arbVar}.applyChanges();
524+
script = ${arbVar}.script;
525+
526+
console.debug(
527+
\`[i] Step ${stepNumber} matched \${${rawMatchesVar}.length} node\${${rawMatchesVar}.length === 1 ? '' : 's'} for ${step?.label ?? 'No Transform'}\`,
528+
);
529+
530+
if (${nextChangesVar} > 0) {
531+
console.debug(
532+
\`[+] Step ${stepNumber} applied \${${nextChangesVar}} change\${${nextChangesVar} === 1 ? '' : 's'}\`,
533+
);
534+
} else {
535+
console.debug(\`[!] Step ${stepNumber} did not change the script\`);
536+
}`;
537+
}
538+
452539
function createStructureSelectionStepBlock(step, stepNumber, resolveStructureFilter) {
453540
const structureId = step?.params?.structureId ?? step?.selectionSource?.structureId ?? '';
454541
const structureTitle = step?.label?.replace(/^(Delete|Isolate|Keep only)\s+/u, '').replace(/\s+matches$/u, '') ||

src/initialScript.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
flASTer usage:
3+
4+
1. Paste JavaScript here or load a local file / saved sample from the top menu.
5+
2. Samples parse automatically. For pasted code or loaded files, click Parse to build the AST and enable structure matching.
6+
3. Use the Structure Explorer to match patterns, inspect results, and open transforms.
7+
4. Apply built-in or custom transforms from the inspector when you are ready.
8+
9+
Replace this comment with the code you want to analyze.
10+
*/
11+
const arr = ['Here,', 'have', 'some', 'interesting', 'nodes', 'for', 'starters'];
12+
console.log(arr.join(' '));

0 commit comments

Comments
 (0)