handlebars-editor-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +3 -0
- package/dist/styles.css +268 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dogukan Incesu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# handlebars-editor-react
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/handlebars-editor-react)
|
|
4
|
+
[](https://github.com/dogukani/handlebars-editor/blob/main/LICENSE)
|
|
5
|
+
[](https://www.npmjs.com/package/handlebars-editor-react)
|
|
6
|
+
|
|
7
|
+
A React component for editing Handlebars templates with syntax highlighting, autocomplete, and error handling.
|
|
8
|
+
|
|
9
|
+
**[Live Demo](https://dogukani.github.io/handlebars-editor)**
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Syntax highlighting for all Handlebars constructs
|
|
14
|
+
- Autocomplete for block helpers and variables
|
|
15
|
+
- Real-time validation with error display
|
|
16
|
+
- Customizable theming via CSS variables
|
|
17
|
+
- TypeScript support
|
|
18
|
+
- Zero dependencies (except React and Handlebars)
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install handlebars-editor-react
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { HandlebarsEditor } from 'handlebars-editor-react';
|
|
30
|
+
import 'handlebars-editor-react/styles.css';
|
|
31
|
+
|
|
32
|
+
function App() {
|
|
33
|
+
const [template, setTemplate] = useState('Hello {{name}}!');
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<HandlebarsEditor
|
|
37
|
+
value={template}
|
|
38
|
+
onChange={setTemplate}
|
|
39
|
+
placeholder="Enter your template..."
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Props
|
|
46
|
+
|
|
47
|
+
| Prop | Type | Default | Description |
|
|
48
|
+
|------|------|---------|-------------|
|
|
49
|
+
| `value` | `string` | required | The template content |
|
|
50
|
+
| `onChange` | `(value: string) => void` | - | Called when content changes |
|
|
51
|
+
| `placeholder` | `string` | `"Enter template..."` | Placeholder text |
|
|
52
|
+
| `readOnly` | `boolean` | `false` | Disable editing |
|
|
53
|
+
| `className` | `string` | `""` | Additional CSS class |
|
|
54
|
+
| `style` | `CSSProperties` | - | Inline styles |
|
|
55
|
+
| `theme` | `Partial<ThemeColors>` | - | Custom colors |
|
|
56
|
+
| `customHelpers` | `string[]` | `[]` | Additional helpers for autocomplete |
|
|
57
|
+
| `autocomplete` | `boolean` | `true` | Enable autocomplete |
|
|
58
|
+
| `showErrors` | `boolean` | `true` | Show error bar |
|
|
59
|
+
| `onError` | `(error: ParseError \| null) => void` | - | Error callback |
|
|
60
|
+
| `errorIcon` | `ReactNode` | - | Custom error icon |
|
|
61
|
+
| `minHeight` | `string \| number` | `100` | Minimum editor height |
|
|
62
|
+
|
|
63
|
+
## Theming
|
|
64
|
+
|
|
65
|
+
Customize colors using CSS variables or the `theme` prop:
|
|
66
|
+
|
|
67
|
+
```css
|
|
68
|
+
.hbs-editor {
|
|
69
|
+
--hbs-color-variable: #3b82f6;
|
|
70
|
+
--hbs-color-helper: #f59e0b;
|
|
71
|
+
--hbs-color-block-keyword: #a855f7;
|
|
72
|
+
--hbs-color-literal: #16a34a;
|
|
73
|
+
--hbs-color-comment: #9ca3af;
|
|
74
|
+
--hbs-color-error: #ef4444;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Or via props:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
<HandlebarsEditor
|
|
82
|
+
value={template}
|
|
83
|
+
theme={{
|
|
84
|
+
variable: '#3b82f6',
|
|
85
|
+
helper: '#f59e0b',
|
|
86
|
+
blockKeyword: '#a855f7',
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Available Theme Colors
|
|
92
|
+
|
|
93
|
+
- `variable` - Simple variables `{{name}}`
|
|
94
|
+
- `variablePath` - Nested paths `{{person.name}}`
|
|
95
|
+
- `blockKeyword` - Block keywords `#if`, `/each`, `else`
|
|
96
|
+
- `blockParam` - Block parameters
|
|
97
|
+
- `helper` - Helper names `{{uppercase name}}`
|
|
98
|
+
- `helperArg` - Helper arguments
|
|
99
|
+
- `hashKey` - Hash keys `key=`
|
|
100
|
+
- `hashValue` - Hash values
|
|
101
|
+
- `literal` - Strings, numbers, booleans
|
|
102
|
+
- `dataVar` - Data variables `@index`, `@key`
|
|
103
|
+
- `subexprParen` - Subexpression parentheses
|
|
104
|
+
- `comment` - Comments
|
|
105
|
+
- `raw` - Raw output `{{{raw}}}`
|
|
106
|
+
- `brace` - Braces `{{` and `}}`
|
|
107
|
+
- `error` - Error highlighting
|
|
108
|
+
- `text` - Default text color
|
|
109
|
+
- `background` - Editor background
|
|
110
|
+
- `caret` - Cursor color
|
|
111
|
+
- `border` - Border color
|
|
112
|
+
- `placeholder` - Placeholder text color
|
|
113
|
+
|
|
114
|
+
### Dark Theme
|
|
115
|
+
|
|
116
|
+
Add the `hbs-theme-dark` class to enable dark mode:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<HandlebarsEditor
|
|
120
|
+
value={template}
|
|
121
|
+
className="hbs-theme-dark"
|
|
122
|
+
/>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Utility Functions
|
|
126
|
+
|
|
127
|
+
The package also exports utility functions for working with Handlebars templates:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { extract, tokenize, validate, interpolate } from 'handlebars-editor-react';
|
|
131
|
+
|
|
132
|
+
// Extract variables from template
|
|
133
|
+
const result = extract('Hello {{name}}, you have {{count}} messages');
|
|
134
|
+
console.log(result.rootVariables); // ['name', 'count']
|
|
135
|
+
|
|
136
|
+
// Tokenize for custom rendering
|
|
137
|
+
const tokens = tokenize('{{#if show}}content{{/if}}');
|
|
138
|
+
|
|
139
|
+
// Validate template syntax
|
|
140
|
+
const { isValid, error } = validate('{{#if unclosed');
|
|
141
|
+
|
|
142
|
+
// Interpolate template with data
|
|
143
|
+
const output = interpolate('Hello {{name}}!', { name: 'World' });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Supported Handlebars Syntax
|
|
147
|
+
|
|
148
|
+
- Simple variables: `{{name}}`
|
|
149
|
+
- Nested paths: `{{person.name}}`
|
|
150
|
+
- Block helpers: `{{#if}}`, `{{#each}}`, `{{#with}}`, `{{#unless}}`
|
|
151
|
+
- Else blocks: `{{else}}`
|
|
152
|
+
- Helpers with arguments: `{{link text url}}`
|
|
153
|
+
- Hash arguments: `{{link "text" href=url}}`
|
|
154
|
+
- Subexpressions: `{{helper (inner arg)}}`
|
|
155
|
+
- Raw output: `{{{unescaped}}}`
|
|
156
|
+
- Comments: `{{! comment }}` and `{{!-- block comment --}}`
|
|
157
|
+
- Data variables: `@index`, `@key`, `@first`, `@last`
|
|
158
|
+
- Parent context: `../`
|
|
159
|
+
- Whitespace control: `{{~trim~}}`
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
164
|
+
|
|
165
|
+
1. Fork the repository
|
|
166
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
167
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
168
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
169
|
+
5. Open a Pull Request
|
|
170
|
+
|
|
171
|
+
### Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Install dependencies
|
|
175
|
+
pnpm install
|
|
176
|
+
|
|
177
|
+
# Run in development mode
|
|
178
|
+
pnpm dev
|
|
179
|
+
|
|
180
|
+
# Run tests
|
|
181
|
+
pnpm test
|
|
182
|
+
|
|
183
|
+
# Build
|
|
184
|
+
pnpm build
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT - see [LICENSE](LICENSE) for details.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
'use strict';Object.defineProperty(exports,'__esModule',{value:true});var react=require('react'),V=require('handlebars'),jsxRuntime=require('react/jsx-runtime');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var V__default=/*#__PURE__*/_interopDefault(V);var Y=V__default.default.Parser,P=Y.lexer,me=Y.terminals_,ge={CONTENT:"text",COMMENT:"comment",OPEN:"brace",CLOSE:"brace",OPEN_UNESCAPED:"brace",CLOSE_UNESCAPED:"brace",OPEN_RAW_BLOCK:"brace",CLOSE_RAW_BLOCK:"brace",END_RAW_BLOCK:"brace",OPEN_BLOCK:"brace",OPEN_INVERSE:"brace",OPEN_INVERSE_CHAIN:"brace",OPEN_ENDBLOCK:"brace",OPEN_PARTIAL:"brace",OPEN_PARTIAL_BLOCK:"brace",OPEN_SEXPR:"subexpr-paren",CLOSE_SEXPR:"subexpr-paren",OPEN_BLOCK_PARAMS:"block-keyword",CLOSE_BLOCK_PARAMS:"block-keyword",INVERSE:"block-keyword",ID:"variable",STRING:"literal",NUMBER:"literal",BOOLEAN:"literal",UNDEFINED:"literal",NULL:"literal",DATA:"data-var",SEP:"text",EQUALS:"text"};function J(r,a,e){let t=0;for(let c=0;c<a-1&&c<r.length;c++)t+=r[c].length+1;return t+e}function Ee(r){let a=[],e=r.split(`
|
|
2
|
+
`);P.setInput(r);try{for(let t=P.lex();t!==P.EOF;t=P.lex()){let c=me[t]||"UNKNOWN",n=P.yylloc;a.push({type:c,text:P.yytext,start:J(e,n.first_line,n.first_column),end:J(e,n.last_line,n.last_column)});}}catch{}return a}function xe(r,a){let e=r[a-1],t=r[a+1];return e&&(e.type==="OPEN_BLOCK"||e.type==="OPEN_INVERSE"||e.type==="OPEN_INVERSE_CHAIN"||e.type==="OPEN_ENDBLOCK")?"block-keyword":e?.type==="OPEN_PARTIAL"||e?.type==="OPEN_PARTIAL_BLOCK"||e?.type==="OPEN_SEXPR"?"helper":e?.type==="OPEN"||e?.type==="OPEN_UNESCAPED"?t&&(t.type==="ID"||t.type==="SEP"||t.type==="STRING"||t.type==="NUMBER"||t.type==="BOOLEAN"||t.type==="DATA"||t.type==="OPEN_SEXPR")?"helper":"variable":e?.type==="EQUALS"?"hash-value":e?.type==="ID"?t?.type==="EQUALS"?"hash-key":"helper-arg":e?.type==="DATA"||e?.type==="SEP"?"helper-arg":e?.type==="OPEN_BLOCK_PARAMS"?"block-param":"variable"}function ye(r,a){let e=[],t=0;for(let c=0;c<a.length;c++){let n=a[c];n.start>t&&e.push({type:"text",value:r.slice(t,n.start),start:t,end:n.start});let b=ge[n.type]||"text";if(n.type==="ID"&&(b=xe(a,c)),n.type==="DATA"){let d=a[c+1];if(d?.type==="ID"&&d.start===n.end){e.push({type:"data-var",value:r.slice(n.start,d.end),start:n.start,end:d.end}),t=d.end,c++;continue}}e.push({type:b,value:r.slice(n.start,n.end),start:n.start,end:n.end}),t=n.end;}return t<r.length&&e.push({type:"text",value:r.slice(t),start:t,end:r.length}),e}function L(r){if(!r)return [];let a=Ee(r);return ye(r,a).filter(e=>e.value.length>0)}var Z=new Set(["if","unless","each","with","lookup","log","this","else"]),M=new Set(["each","with"]);function K(r){return Z.has(r)||r.startsWith("@")}var ee=V__default.default.Parser,O=ee.lexer,Oe=ee.terminals_;function Ne(r){let a=new Map,e=r;for(;e.length>0;){let t=ke(e,a);if(t===e.length)break;let c=e.slice(t),n=c.search(/\{\{/);if(n===-1)break;e=c.slice(n);}return Array.from(a.values())}function ke(r,a){O.setInput(r),O.conditionStack=["INITIAL"];let e=0,t=false,c=null,n=[],b=[];function d(i,l){let f=[];for(let o=l;o<i.length;o++)if(i[o].name==="ID")f.push(i[o].text);else if(i[o].name!=="SEP")break;return f}function x(i,l,f){a.has(l)||a.set(l,{name:i,path:l,type:f?"block":l.includes(".")?"nested":"simple",blockType:f});}function _(){if(n.length===0)return;let i=0;if(n[i]?.name==="DATA")return;let l=n[i].text;if(c==="block"){let f=b.some(o=>M.has(o.helper));if(M.has(l)){for(i++;i<n.length&&n[i].name!=="ID";)i++;if(i<n.length){let o=d(n,i);o.length>0&&!K(o[0])&&!f&&x(o[0],o.join("."),l),b.push({helper:l,context:o[0]||""});}}else if(Z.has(l)){for(i++;i<n.length&&n[i].name!=="ID";)i++;if(i<n.length){let o=d(n,i);o.length>0&&!K(o[0])&&!f&&x(o[0],o.join("."),l);}b.push({helper:l,context:""});}else b.push({helper:l,context:""});}else if(c==="endblock")b.length>0&&b.pop();else {let f=b.some(o=>M.has(o.helper));if(!K(l)&&!f){let o=d(n,i);o.length>0&&x(o[0],o.join("."));}}}try{for(let i=O.lex();i!==O.EOF;i=O.lex()){let l=Oe[i],f=O.yytext;if(e=O.yylloc?.last_column??e,!(l==="OPEN_RAW_BLOCK"||l==="CLOSE_RAW_BLOCK"||l==="END_RAW_BLOCK")){if(l==="OPEN"||l==="OPEN_UNESCAPED"){t=!0,c="mustache",n=[];continue}if(l==="OPEN_BLOCK"||l==="OPEN_INVERSE"||l==="OPEN_INVERSE_CHAIN"){t=!0,c="block",n=[];continue}if(l==="OPEN_ENDBLOCK"){t=!0,c="endblock",n=[];continue}if(l==="CLOSE"||l==="CLOSE_UNESCAPED"){_(),t=!1,n=[];continue}t&&n.push({name:l,text:f});}}return r.length}catch{return e}}function U(r){let a=Ne(r),e=[...new Set(a.map(t=>t.name))];return {variables:a,rootVariables:e}}function Pe(r,a,e){if(!a)return r;let t=e?.helpers?V__default.default.create():V__default.default;if(e?.helpers)for(let[c,n]of Object.entries(e.helpers))t.registerHelper(c,n);return t.compile(r)(a)}var Se=["#if","#each","#with","#unless","/if","/each","/with","/unless","else"];function re({value:r,onChange:a,placeholder:e="Enter template...",readOnly:t=false,className:c="",style:n,theme:b,customHelpers:d=[],autocomplete:x=true,minHeight:_=100}){let i=react.useRef(null),l=react.useRef(null),f=react.useRef(null),[o,y]=react.useState({isOpen:false,options:[],selectedIndex:0,triggerStart:0,filterText:""}),[R,se]=react.useState({top:0,left:0}),z=react.useMemo(()=>U(r).rootVariables,[r]),F=react.useMemo(()=>[...Se,...d,...z],[d,z]),$=react.useMemo(()=>L(r),[r]),oe=react.useMemo(()=>r?$.map(s=>s.type==="text"?s.value:jsxRuntime.jsx("span",{className:`hbs-token-${s.type}`,children:s.value},s.start)):jsxRuntime.jsx("span",{className:"hbs-editor-placeholder",children:e}),[$,r,e]),ae=react.useCallback(s=>{let p=s.target.value;if(a?.(p),!x||t)return;let h=s.target.selectionStart,m=p.slice(0,h).match(/\{\{([#/]?\w*)$/);if(m){let g=m[1]||"",E=h-g.length,N=F.filter(w=>w.toLowerCase().startsWith(g.toLowerCase()));if(N.length>0){y({isOpen:true,options:N,selectedIndex:0,triggerStart:E,filterText:g});return}}y(g=>({...g,isOpen:false}));},[a,x,t,F]),v=react.useCallback(s=>{let p=i.current;if(!p)return;let{triggerStart:h}=o,u=s,m=s.length;if(s.startsWith("#")){let E=s.slice(1);u=`${s} }}{{/${E}}}`,m=s.length+1;}else !s.startsWith("/")&&s!=="else"?(u=`${s}}}`,m=u.length):(u=`${s}}}`,m=u.length);p.focus(),p.setSelectionRange(h,p.selectionStart),document.execCommand("insertText",false,u);let g=h+m;p.setSelectionRange(g,g),a?.(p.value),y(E=>({...E,isOpen:false}));},[a,o]),le=react.useCallback(s=>{let{scrollTop:p,scrollLeft:h}=s.currentTarget;l.current&&(l.current.scrollTop=p,l.current.scrollLeft=h),se({top:p,left:h});},[]),ie=react.useCallback(s=>{if(!o.isOpen)return;let{options:p,selectedIndex:h}=o;switch(s.key){case "ArrowDown":s.preventDefault(),y(u=>({...u,selectedIndex:Math.min(h+1,p.length-1)}));break;case "ArrowUp":s.preventDefault(),y(u=>({...u,selectedIndex:Math.max(h-1,0)}));break;case "Enter":case "Tab":s.preventDefault(),p[h]&&v(p[h]);break;case "Escape":s.preventDefault(),y(u=>({...u,isOpen:false}));break}},[o,v]),I=react.useMemo(()=>{if(!o.isOpen||!i.current)return {top:0,left:0,maxHeight:192,openUpward:false,visible:false};let s=i.current,{triggerStart:p}=o,u=r.slice(0,p).split(`
|
|
3
|
+
`),m=u.length,g=u[u.length-1]?.length||0,E=getComputedStyle(s),N=parseFloat(E.lineHeight)||20,w=parseFloat(E.paddingTop)||0,he=parseFloat(E.paddingLeft)||0,ue=(parseFloat(E.fontSize)||14)*.6,X=getComputedStyle(s.parentElement||s),fe=parseInt(X.getPropertyValue("--hbs-autocomplete-min-width"))||180,j=parseInt(X.getPropertyValue("--hbs-autocomplete-max-height"))||192,k=w+m*N-R.top,A=he+g*ue-R.left,Q=s.clientWidth-fe-10;A>Q&&(A=Math.max(10,Q));let D=s.clientHeight-k-10,G=k-N-10,q=D<120&&G>D,B,C;q?(C=Math.min(j,Math.max(80,G)),B=k-N-C):(C=Math.min(j,Math.max(80,D)),B=k);let be=k>0&&k<s.clientHeight&&A>0&&A<s.clientWidth;return {top:B,left:A,maxHeight:C,openUpward:q,visible:be}},[o,r,R]),ce=react.useMemo(()=>{if(!b)return {};let s={},p={variable:"--hbs-color-variable",variablePath:"--hbs-color-variable-path",blockKeyword:"--hbs-color-block-keyword",blockParam:"--hbs-color-block-param",helper:"--hbs-color-helper",helperArg:"--hbs-color-helper-arg",hashKey:"--hbs-color-hash-key",hashValue:"--hbs-color-hash-value",literal:"--hbs-color-literal",dataVar:"--hbs-color-data-var",subexprParen:"--hbs-color-subexpr-paren",comment:"--hbs-color-comment",raw:"--hbs-color-raw",brace:"--hbs-color-brace",text:"--hbs-color-text",background:"--hbs-color-background",caret:"--hbs-color-caret",border:"--hbs-color-border",placeholder:"--hbs-color-placeholder"};for(let[h,u]of Object.entries(p)){let m=b[h];m&&(s[u]=m);}return s},[b]);react.useEffect(()=>{if(!o.isOpen||!f.current)return;let h=f.current.querySelectorAll(".hbs-autocomplete-item")[o.selectedIndex];h&&h.scrollIntoView({block:"nearest"});},[o.selectedIndex,o.isOpen]);let pe=["hbs-editor",t?"hbs-readonly":"",c].filter(Boolean).join(" ");return jsxRuntime.jsxs("div",{className:pe,style:{...ce,minHeight:typeof _=="number"?`${_}px`:_,...n},children:[jsxRuntime.jsx("div",{ref:l,className:"hbs-editor-highlight",children:oe}),jsxRuntime.jsx("textarea",{ref:i,className:"hbs-editor-textarea",value:r,onChange:ae,onKeyDown:ie,onScroll:le,placeholder:"",readOnly:t,spellCheck:false,autoCapitalize:"off",autoComplete:"off",autoCorrect:"off"}),x&&o.isOpen&&I.visible&&jsxRuntime.jsx("div",{ref:f,className:"hbs-autocomplete",style:{top:I.top,left:I.left,maxHeight:I.maxHeight},children:o.options.map((s,p)=>jsxRuntime.jsx("button",{type:"button",className:`hbs-autocomplete-item ${p===o.selectedIndex?"hbs-selected":""}`,onClick:()=>v(s),onMouseEnter:()=>y(h=>({...h,selectedIndex:p})),children:s},s))})]})}function _e({content:r,className:a}){if(!r)return null;let e=L(r);return jsxRuntime.jsx("span",{className:a,children:e.map(t=>jsxRuntime.jsx("span",{className:`hbs-token-${t.type}`,children:t.value},t.start))})}exports.HandlebarsEditor=re;exports.HandlebarsHighlight=_e;exports.default=re;exports.extract=U;exports.interpolate=Pe;exports.tokenize=L;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extracted variable/input from a Handlebars template
|
|
6
|
+
*/
|
|
7
|
+
interface ExtractedVariable {
|
|
8
|
+
/** Root variable name (e.g., "person" for "person.name") */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Full path (e.g., "person.name") */
|
|
11
|
+
path: string;
|
|
12
|
+
/** Type of variable */
|
|
13
|
+
type: 'simple' | 'nested' | 'block';
|
|
14
|
+
/** Block type if this is a block helper parameter */
|
|
15
|
+
blockType?: 'if' | 'each' | 'with' | 'unless';
|
|
16
|
+
/** Context path if inside a block (e.g., "items[]" for vars inside #each items) */
|
|
17
|
+
context?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Result of extracting variables from a template
|
|
21
|
+
*/
|
|
22
|
+
interface ExtractionResult {
|
|
23
|
+
/** All extracted variables with full details */
|
|
24
|
+
variables: ExtractedVariable[];
|
|
25
|
+
/** Unique root variable names */
|
|
26
|
+
rootVariables: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Token types for syntax highlighting
|
|
30
|
+
*/
|
|
31
|
+
type TokenType = 'text' | 'variable' | 'variable-path' | 'block-keyword' | 'block-param' | 'helper' | 'helper-arg' | 'hash-key' | 'hash-value' | 'literal' | 'data-var' | 'subexpr-paren' | 'comment' | 'raw' | 'brace';
|
|
32
|
+
/**
|
|
33
|
+
* A single token from tokenization
|
|
34
|
+
*/
|
|
35
|
+
interface HighlightToken {
|
|
36
|
+
type: TokenType;
|
|
37
|
+
value: string;
|
|
38
|
+
start: number;
|
|
39
|
+
end: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Theme colors for syntax highlighting
|
|
43
|
+
*/
|
|
44
|
+
interface ThemeColors {
|
|
45
|
+
/** Simple variables like {{name}} */
|
|
46
|
+
variable?: string;
|
|
47
|
+
/** Nested paths like {{person.name}} */
|
|
48
|
+
variablePath?: string;
|
|
49
|
+
/** Block keywords like #if, /each, else */
|
|
50
|
+
blockKeyword?: string;
|
|
51
|
+
/** Block parameters */
|
|
52
|
+
blockParam?: string;
|
|
53
|
+
/** Helper names like {{uppercase name}} */
|
|
54
|
+
helper?: string;
|
|
55
|
+
/** Helper arguments */
|
|
56
|
+
helperArg?: string;
|
|
57
|
+
/** Hash keys like key= in {{helper key=value}} */
|
|
58
|
+
hashKey?: string;
|
|
59
|
+
/** Hash values */
|
|
60
|
+
hashValue?: string;
|
|
61
|
+
/** Literals like "string", 123, true */
|
|
62
|
+
literal?: string;
|
|
63
|
+
/** Data variables like @index, @key */
|
|
64
|
+
dataVar?: string;
|
|
65
|
+
/** Subexpression parentheses */
|
|
66
|
+
subexprParen?: string;
|
|
67
|
+
/** Comments */
|
|
68
|
+
comment?: string;
|
|
69
|
+
/** Raw output {{{raw}}} */
|
|
70
|
+
raw?: string;
|
|
71
|
+
/** Braces {{ and }} */
|
|
72
|
+
brace?: string;
|
|
73
|
+
/** Text color */
|
|
74
|
+
text?: string;
|
|
75
|
+
/** Background color */
|
|
76
|
+
background?: string;
|
|
77
|
+
/** Caret/cursor color */
|
|
78
|
+
caret?: string;
|
|
79
|
+
/** Border color */
|
|
80
|
+
border?: string;
|
|
81
|
+
/** Placeholder text color */
|
|
82
|
+
placeholder?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Props for the HandlebarsEditor component
|
|
86
|
+
*/
|
|
87
|
+
interface HandlebarsEditorProps {
|
|
88
|
+
/** Current template value */
|
|
89
|
+
value: string;
|
|
90
|
+
/** Called when value changes */
|
|
91
|
+
onChange?: (value: string) => void;
|
|
92
|
+
/** Placeholder text when empty */
|
|
93
|
+
placeholder?: string;
|
|
94
|
+
/** Whether the editor is read-only */
|
|
95
|
+
readOnly?: boolean;
|
|
96
|
+
/** Custom class name */
|
|
97
|
+
className?: string;
|
|
98
|
+
/** Inline styles */
|
|
99
|
+
style?: CSSProperties;
|
|
100
|
+
/** Theme colors (overrides CSS variables) */
|
|
101
|
+
theme?: Partial<ThemeColors>;
|
|
102
|
+
/** Additional helpers to show in autocomplete */
|
|
103
|
+
customHelpers?: string[];
|
|
104
|
+
/** Whether to show autocomplete */
|
|
105
|
+
autocomplete?: boolean;
|
|
106
|
+
/** Minimum height */
|
|
107
|
+
minHeight?: string | number;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Autocomplete state
|
|
111
|
+
*/
|
|
112
|
+
interface AutocompleteState {
|
|
113
|
+
isOpen: boolean;
|
|
114
|
+
options: string[];
|
|
115
|
+
selectedIndex: number;
|
|
116
|
+
triggerStart: number;
|
|
117
|
+
filterText: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Handlebars template editor with syntax highlighting
|
|
122
|
+
*/
|
|
123
|
+
declare function HandlebarsEditor({ value, onChange, placeholder, readOnly, className, style, theme, customHelpers, autocomplete, minHeight, }: HandlebarsEditorProps): react_jsx_runtime.JSX.Element;
|
|
124
|
+
|
|
125
|
+
interface HandlebarsHighlightProps {
|
|
126
|
+
content: string;
|
|
127
|
+
className?: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Lightweight inline syntax highlighting for Handlebars content.
|
|
131
|
+
* Renders colored spans without any wrapper - embed within your own containers.
|
|
132
|
+
*/
|
|
133
|
+
declare function HandlebarsHighlight({ content, className }: HandlebarsHighlightProps): react_jsx_runtime.JSX.Element | null;
|
|
134
|
+
|
|
135
|
+
declare function tokenize(content: string): HighlightToken[];
|
|
136
|
+
|
|
137
|
+
declare function extract(content: string): ExtractionResult;
|
|
138
|
+
interface InterpolateOptions {
|
|
139
|
+
helpers?: Record<string, (...args: unknown[]) => unknown>;
|
|
140
|
+
}
|
|
141
|
+
declare function interpolate(content: string, variables?: Record<string, unknown>, options?: InterpolateOptions): string;
|
|
142
|
+
|
|
143
|
+
export { type AutocompleteState, type ExtractedVariable, type ExtractionResult, HandlebarsEditor, type HandlebarsEditorProps, HandlebarsHighlight, type HighlightToken, type InterpolateOptions, type ThemeColors, type TokenType, HandlebarsEditor as default, extract, interpolate, tokenize };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extracted variable/input from a Handlebars template
|
|
6
|
+
*/
|
|
7
|
+
interface ExtractedVariable {
|
|
8
|
+
/** Root variable name (e.g., "person" for "person.name") */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Full path (e.g., "person.name") */
|
|
11
|
+
path: string;
|
|
12
|
+
/** Type of variable */
|
|
13
|
+
type: 'simple' | 'nested' | 'block';
|
|
14
|
+
/** Block type if this is a block helper parameter */
|
|
15
|
+
blockType?: 'if' | 'each' | 'with' | 'unless';
|
|
16
|
+
/** Context path if inside a block (e.g., "items[]" for vars inside #each items) */
|
|
17
|
+
context?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Result of extracting variables from a template
|
|
21
|
+
*/
|
|
22
|
+
interface ExtractionResult {
|
|
23
|
+
/** All extracted variables with full details */
|
|
24
|
+
variables: ExtractedVariable[];
|
|
25
|
+
/** Unique root variable names */
|
|
26
|
+
rootVariables: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Token types for syntax highlighting
|
|
30
|
+
*/
|
|
31
|
+
type TokenType = 'text' | 'variable' | 'variable-path' | 'block-keyword' | 'block-param' | 'helper' | 'helper-arg' | 'hash-key' | 'hash-value' | 'literal' | 'data-var' | 'subexpr-paren' | 'comment' | 'raw' | 'brace';
|
|
32
|
+
/**
|
|
33
|
+
* A single token from tokenization
|
|
34
|
+
*/
|
|
35
|
+
interface HighlightToken {
|
|
36
|
+
type: TokenType;
|
|
37
|
+
value: string;
|
|
38
|
+
start: number;
|
|
39
|
+
end: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Theme colors for syntax highlighting
|
|
43
|
+
*/
|
|
44
|
+
interface ThemeColors {
|
|
45
|
+
/** Simple variables like {{name}} */
|
|
46
|
+
variable?: string;
|
|
47
|
+
/** Nested paths like {{person.name}} */
|
|
48
|
+
variablePath?: string;
|
|
49
|
+
/** Block keywords like #if, /each, else */
|
|
50
|
+
blockKeyword?: string;
|
|
51
|
+
/** Block parameters */
|
|
52
|
+
blockParam?: string;
|
|
53
|
+
/** Helper names like {{uppercase name}} */
|
|
54
|
+
helper?: string;
|
|
55
|
+
/** Helper arguments */
|
|
56
|
+
helperArg?: string;
|
|
57
|
+
/** Hash keys like key= in {{helper key=value}} */
|
|
58
|
+
hashKey?: string;
|
|
59
|
+
/** Hash values */
|
|
60
|
+
hashValue?: string;
|
|
61
|
+
/** Literals like "string", 123, true */
|
|
62
|
+
literal?: string;
|
|
63
|
+
/** Data variables like @index, @key */
|
|
64
|
+
dataVar?: string;
|
|
65
|
+
/** Subexpression parentheses */
|
|
66
|
+
subexprParen?: string;
|
|
67
|
+
/** Comments */
|
|
68
|
+
comment?: string;
|
|
69
|
+
/** Raw output {{{raw}}} */
|
|
70
|
+
raw?: string;
|
|
71
|
+
/** Braces {{ and }} */
|
|
72
|
+
brace?: string;
|
|
73
|
+
/** Text color */
|
|
74
|
+
text?: string;
|
|
75
|
+
/** Background color */
|
|
76
|
+
background?: string;
|
|
77
|
+
/** Caret/cursor color */
|
|
78
|
+
caret?: string;
|
|
79
|
+
/** Border color */
|
|
80
|
+
border?: string;
|
|
81
|
+
/** Placeholder text color */
|
|
82
|
+
placeholder?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Props for the HandlebarsEditor component
|
|
86
|
+
*/
|
|
87
|
+
interface HandlebarsEditorProps {
|
|
88
|
+
/** Current template value */
|
|
89
|
+
value: string;
|
|
90
|
+
/** Called when value changes */
|
|
91
|
+
onChange?: (value: string) => void;
|
|
92
|
+
/** Placeholder text when empty */
|
|
93
|
+
placeholder?: string;
|
|
94
|
+
/** Whether the editor is read-only */
|
|
95
|
+
readOnly?: boolean;
|
|
96
|
+
/** Custom class name */
|
|
97
|
+
className?: string;
|
|
98
|
+
/** Inline styles */
|
|
99
|
+
style?: CSSProperties;
|
|
100
|
+
/** Theme colors (overrides CSS variables) */
|
|
101
|
+
theme?: Partial<ThemeColors>;
|
|
102
|
+
/** Additional helpers to show in autocomplete */
|
|
103
|
+
customHelpers?: string[];
|
|
104
|
+
/** Whether to show autocomplete */
|
|
105
|
+
autocomplete?: boolean;
|
|
106
|
+
/** Minimum height */
|
|
107
|
+
minHeight?: string | number;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Autocomplete state
|
|
111
|
+
*/
|
|
112
|
+
interface AutocompleteState {
|
|
113
|
+
isOpen: boolean;
|
|
114
|
+
options: string[];
|
|
115
|
+
selectedIndex: number;
|
|
116
|
+
triggerStart: number;
|
|
117
|
+
filterText: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Handlebars template editor with syntax highlighting
|
|
122
|
+
*/
|
|
123
|
+
declare function HandlebarsEditor({ value, onChange, placeholder, readOnly, className, style, theme, customHelpers, autocomplete, minHeight, }: HandlebarsEditorProps): react_jsx_runtime.JSX.Element;
|
|
124
|
+
|
|
125
|
+
interface HandlebarsHighlightProps {
|
|
126
|
+
content: string;
|
|
127
|
+
className?: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Lightweight inline syntax highlighting for Handlebars content.
|
|
131
|
+
* Renders colored spans without any wrapper - embed within your own containers.
|
|
132
|
+
*/
|
|
133
|
+
declare function HandlebarsHighlight({ content, className }: HandlebarsHighlightProps): react_jsx_runtime.JSX.Element | null;
|
|
134
|
+
|
|
135
|
+
declare function tokenize(content: string): HighlightToken[];
|
|
136
|
+
|
|
137
|
+
declare function extract(content: string): ExtractionResult;
|
|
138
|
+
interface InterpolateOptions {
|
|
139
|
+
helpers?: Record<string, (...args: unknown[]) => unknown>;
|
|
140
|
+
}
|
|
141
|
+
declare function interpolate(content: string, variables?: Record<string, unknown>, options?: InterpolateOptions): string;
|
|
142
|
+
|
|
143
|
+
export { type AutocompleteState, type ExtractedVariable, type ExtractionResult, HandlebarsEditor, type HandlebarsEditorProps, HandlebarsHighlight, type HighlightToken, type InterpolateOptions, type ThemeColors, type TokenType, HandlebarsEditor as default, extract, interpolate, tokenize };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import {useRef,useState,useMemo,useCallback,useEffect}from'react';import V from'handlebars';import {jsx,jsxs}from'react/jsx-runtime';var Y=V.Parser,P=Y.lexer,me=Y.terminals_,ge={CONTENT:"text",COMMENT:"comment",OPEN:"brace",CLOSE:"brace",OPEN_UNESCAPED:"brace",CLOSE_UNESCAPED:"brace",OPEN_RAW_BLOCK:"brace",CLOSE_RAW_BLOCK:"brace",END_RAW_BLOCK:"brace",OPEN_BLOCK:"brace",OPEN_INVERSE:"brace",OPEN_INVERSE_CHAIN:"brace",OPEN_ENDBLOCK:"brace",OPEN_PARTIAL:"brace",OPEN_PARTIAL_BLOCK:"brace",OPEN_SEXPR:"subexpr-paren",CLOSE_SEXPR:"subexpr-paren",OPEN_BLOCK_PARAMS:"block-keyword",CLOSE_BLOCK_PARAMS:"block-keyword",INVERSE:"block-keyword",ID:"variable",STRING:"literal",NUMBER:"literal",BOOLEAN:"literal",UNDEFINED:"literal",NULL:"literal",DATA:"data-var",SEP:"text",EQUALS:"text"};function J(r,a,e){let t=0;for(let c=0;c<a-1&&c<r.length;c++)t+=r[c].length+1;return t+e}function Ee(r){let a=[],e=r.split(`
|
|
2
|
+
`);P.setInput(r);try{for(let t=P.lex();t!==P.EOF;t=P.lex()){let c=me[t]||"UNKNOWN",n=P.yylloc;a.push({type:c,text:P.yytext,start:J(e,n.first_line,n.first_column),end:J(e,n.last_line,n.last_column)});}}catch{}return a}function xe(r,a){let e=r[a-1],t=r[a+1];return e&&(e.type==="OPEN_BLOCK"||e.type==="OPEN_INVERSE"||e.type==="OPEN_INVERSE_CHAIN"||e.type==="OPEN_ENDBLOCK")?"block-keyword":e?.type==="OPEN_PARTIAL"||e?.type==="OPEN_PARTIAL_BLOCK"||e?.type==="OPEN_SEXPR"?"helper":e?.type==="OPEN"||e?.type==="OPEN_UNESCAPED"?t&&(t.type==="ID"||t.type==="SEP"||t.type==="STRING"||t.type==="NUMBER"||t.type==="BOOLEAN"||t.type==="DATA"||t.type==="OPEN_SEXPR")?"helper":"variable":e?.type==="EQUALS"?"hash-value":e?.type==="ID"?t?.type==="EQUALS"?"hash-key":"helper-arg":e?.type==="DATA"||e?.type==="SEP"?"helper-arg":e?.type==="OPEN_BLOCK_PARAMS"?"block-param":"variable"}function ye(r,a){let e=[],t=0;for(let c=0;c<a.length;c++){let n=a[c];n.start>t&&e.push({type:"text",value:r.slice(t,n.start),start:t,end:n.start});let b=ge[n.type]||"text";if(n.type==="ID"&&(b=xe(a,c)),n.type==="DATA"){let d=a[c+1];if(d?.type==="ID"&&d.start===n.end){e.push({type:"data-var",value:r.slice(n.start,d.end),start:n.start,end:d.end}),t=d.end,c++;continue}}e.push({type:b,value:r.slice(n.start,n.end),start:n.start,end:n.end}),t=n.end;}return t<r.length&&e.push({type:"text",value:r.slice(t),start:t,end:r.length}),e}function L(r){if(!r)return [];let a=Ee(r);return ye(r,a).filter(e=>e.value.length>0)}var Z=new Set(["if","unless","each","with","lookup","log","this","else"]),M=new Set(["each","with"]);function K(r){return Z.has(r)||r.startsWith("@")}var ee=V.Parser,O=ee.lexer,Oe=ee.terminals_;function Ne(r){let a=new Map,e=r;for(;e.length>0;){let t=ke(e,a);if(t===e.length)break;let c=e.slice(t),n=c.search(/\{\{/);if(n===-1)break;e=c.slice(n);}return Array.from(a.values())}function ke(r,a){O.setInput(r),O.conditionStack=["INITIAL"];let e=0,t=false,c=null,n=[],b=[];function d(i,l){let f=[];for(let o=l;o<i.length;o++)if(i[o].name==="ID")f.push(i[o].text);else if(i[o].name!=="SEP")break;return f}function x(i,l,f){a.has(l)||a.set(l,{name:i,path:l,type:f?"block":l.includes(".")?"nested":"simple",blockType:f});}function _(){if(n.length===0)return;let i=0;if(n[i]?.name==="DATA")return;let l=n[i].text;if(c==="block"){let f=b.some(o=>M.has(o.helper));if(M.has(l)){for(i++;i<n.length&&n[i].name!=="ID";)i++;if(i<n.length){let o=d(n,i);o.length>0&&!K(o[0])&&!f&&x(o[0],o.join("."),l),b.push({helper:l,context:o[0]||""});}}else if(Z.has(l)){for(i++;i<n.length&&n[i].name!=="ID";)i++;if(i<n.length){let o=d(n,i);o.length>0&&!K(o[0])&&!f&&x(o[0],o.join("."),l);}b.push({helper:l,context:""});}else b.push({helper:l,context:""});}else if(c==="endblock")b.length>0&&b.pop();else {let f=b.some(o=>M.has(o.helper));if(!K(l)&&!f){let o=d(n,i);o.length>0&&x(o[0],o.join("."));}}}try{for(let i=O.lex();i!==O.EOF;i=O.lex()){let l=Oe[i],f=O.yytext;if(e=O.yylloc?.last_column??e,!(l==="OPEN_RAW_BLOCK"||l==="CLOSE_RAW_BLOCK"||l==="END_RAW_BLOCK")){if(l==="OPEN"||l==="OPEN_UNESCAPED"){t=!0,c="mustache",n=[];continue}if(l==="OPEN_BLOCK"||l==="OPEN_INVERSE"||l==="OPEN_INVERSE_CHAIN"){t=!0,c="block",n=[];continue}if(l==="OPEN_ENDBLOCK"){t=!0,c="endblock",n=[];continue}if(l==="CLOSE"||l==="CLOSE_UNESCAPED"){_(),t=!1,n=[];continue}t&&n.push({name:l,text:f});}}return r.length}catch{return e}}function U(r){let a=Ne(r),e=[...new Set(a.map(t=>t.name))];return {variables:a,rootVariables:e}}function Pe(r,a,e){if(!a)return r;let t=e?.helpers?V.create():V;if(e?.helpers)for(let[c,n]of Object.entries(e.helpers))t.registerHelper(c,n);return t.compile(r)(a)}var Se=["#if","#each","#with","#unless","/if","/each","/with","/unless","else"];function re({value:r,onChange:a,placeholder:e="Enter template...",readOnly:t=false,className:c="",style:n,theme:b,customHelpers:d=[],autocomplete:x=true,minHeight:_=100}){let i=useRef(null),l=useRef(null),f=useRef(null),[o,y]=useState({isOpen:false,options:[],selectedIndex:0,triggerStart:0,filterText:""}),[R,se]=useState({top:0,left:0}),z=useMemo(()=>U(r).rootVariables,[r]),F=useMemo(()=>[...Se,...d,...z],[d,z]),$=useMemo(()=>L(r),[r]),oe=useMemo(()=>r?$.map(s=>s.type==="text"?s.value:jsx("span",{className:`hbs-token-${s.type}`,children:s.value},s.start)):jsx("span",{className:"hbs-editor-placeholder",children:e}),[$,r,e]),ae=useCallback(s=>{let p=s.target.value;if(a?.(p),!x||t)return;let h=s.target.selectionStart,m=p.slice(0,h).match(/\{\{([#/]?\w*)$/);if(m){let g=m[1]||"",E=h-g.length,N=F.filter(w=>w.toLowerCase().startsWith(g.toLowerCase()));if(N.length>0){y({isOpen:true,options:N,selectedIndex:0,triggerStart:E,filterText:g});return}}y(g=>({...g,isOpen:false}));},[a,x,t,F]),v=useCallback(s=>{let p=i.current;if(!p)return;let{triggerStart:h}=o,u=s,m=s.length;if(s.startsWith("#")){let E=s.slice(1);u=`${s} }}{{/${E}}}`,m=s.length+1;}else !s.startsWith("/")&&s!=="else"?(u=`${s}}}`,m=u.length):(u=`${s}}}`,m=u.length);p.focus(),p.setSelectionRange(h,p.selectionStart),document.execCommand("insertText",false,u);let g=h+m;p.setSelectionRange(g,g),a?.(p.value),y(E=>({...E,isOpen:false}));},[a,o]),le=useCallback(s=>{let{scrollTop:p,scrollLeft:h}=s.currentTarget;l.current&&(l.current.scrollTop=p,l.current.scrollLeft=h),se({top:p,left:h});},[]),ie=useCallback(s=>{if(!o.isOpen)return;let{options:p,selectedIndex:h}=o;switch(s.key){case "ArrowDown":s.preventDefault(),y(u=>({...u,selectedIndex:Math.min(h+1,p.length-1)}));break;case "ArrowUp":s.preventDefault(),y(u=>({...u,selectedIndex:Math.max(h-1,0)}));break;case "Enter":case "Tab":s.preventDefault(),p[h]&&v(p[h]);break;case "Escape":s.preventDefault(),y(u=>({...u,isOpen:false}));break}},[o,v]),I=useMemo(()=>{if(!o.isOpen||!i.current)return {top:0,left:0,maxHeight:192,openUpward:false,visible:false};let s=i.current,{triggerStart:p}=o,u=r.slice(0,p).split(`
|
|
3
|
+
`),m=u.length,g=u[u.length-1]?.length||0,E=getComputedStyle(s),N=parseFloat(E.lineHeight)||20,w=parseFloat(E.paddingTop)||0,he=parseFloat(E.paddingLeft)||0,ue=(parseFloat(E.fontSize)||14)*.6,X=getComputedStyle(s.parentElement||s),fe=parseInt(X.getPropertyValue("--hbs-autocomplete-min-width"))||180,j=parseInt(X.getPropertyValue("--hbs-autocomplete-max-height"))||192,k=w+m*N-R.top,A=he+g*ue-R.left,Q=s.clientWidth-fe-10;A>Q&&(A=Math.max(10,Q));let D=s.clientHeight-k-10,G=k-N-10,q=D<120&&G>D,B,C;q?(C=Math.min(j,Math.max(80,G)),B=k-N-C):(C=Math.min(j,Math.max(80,D)),B=k);let be=k>0&&k<s.clientHeight&&A>0&&A<s.clientWidth;return {top:B,left:A,maxHeight:C,openUpward:q,visible:be}},[o,r,R]),ce=useMemo(()=>{if(!b)return {};let s={},p={variable:"--hbs-color-variable",variablePath:"--hbs-color-variable-path",blockKeyword:"--hbs-color-block-keyword",blockParam:"--hbs-color-block-param",helper:"--hbs-color-helper",helperArg:"--hbs-color-helper-arg",hashKey:"--hbs-color-hash-key",hashValue:"--hbs-color-hash-value",literal:"--hbs-color-literal",dataVar:"--hbs-color-data-var",subexprParen:"--hbs-color-subexpr-paren",comment:"--hbs-color-comment",raw:"--hbs-color-raw",brace:"--hbs-color-brace",text:"--hbs-color-text",background:"--hbs-color-background",caret:"--hbs-color-caret",border:"--hbs-color-border",placeholder:"--hbs-color-placeholder"};for(let[h,u]of Object.entries(p)){let m=b[h];m&&(s[u]=m);}return s},[b]);useEffect(()=>{if(!o.isOpen||!f.current)return;let h=f.current.querySelectorAll(".hbs-autocomplete-item")[o.selectedIndex];h&&h.scrollIntoView({block:"nearest"});},[o.selectedIndex,o.isOpen]);let pe=["hbs-editor",t?"hbs-readonly":"",c].filter(Boolean).join(" ");return jsxs("div",{className:pe,style:{...ce,minHeight:typeof _=="number"?`${_}px`:_,...n},children:[jsx("div",{ref:l,className:"hbs-editor-highlight",children:oe}),jsx("textarea",{ref:i,className:"hbs-editor-textarea",value:r,onChange:ae,onKeyDown:ie,onScroll:le,placeholder:"",readOnly:t,spellCheck:false,autoCapitalize:"off",autoComplete:"off",autoCorrect:"off"}),x&&o.isOpen&&I.visible&&jsx("div",{ref:f,className:"hbs-autocomplete",style:{top:I.top,left:I.left,maxHeight:I.maxHeight},children:o.options.map((s,p)=>jsx("button",{type:"button",className:`hbs-autocomplete-item ${p===o.selectedIndex?"hbs-selected":""}`,onClick:()=>v(s),onMouseEnter:()=>y(h=>({...h,selectedIndex:p})),children:s},s))})]})}function _e({content:r,className:a}){if(!r)return null;let e=L(r);return jsx("span",{className:a,children:e.map(t=>jsx("span",{className:`hbs-token-${t.type}`,children:t.value},t.start))})}export{re as HandlebarsEditor,_e as HandlebarsHighlight,re as default,U as extract,Pe as interpolate,L as tokenize};
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/* Handlebars Editor - Default Theme */
|
|
2
|
+
|
|
3
|
+
.hbs-editor {
|
|
4
|
+
/* Colors - customize these CSS variables */
|
|
5
|
+
--hbs-color-text: #1f2937;
|
|
6
|
+
--hbs-color-background: #ffffff;
|
|
7
|
+
--hbs-color-border: #e5e7eb;
|
|
8
|
+
--hbs-color-caret: #1f2937;
|
|
9
|
+
--hbs-color-placeholder: #9ca3af;
|
|
10
|
+
--hbs-color-focus-ring: #3b82f6;
|
|
11
|
+
|
|
12
|
+
/* Syntax highlighting colors */
|
|
13
|
+
--hbs-color-variable: #3b82f6;
|
|
14
|
+
--hbs-color-variable-path: #06b6d4;
|
|
15
|
+
--hbs-color-block-keyword: #a855f7;
|
|
16
|
+
--hbs-color-block-param: #3b82f6;
|
|
17
|
+
--hbs-color-helper: #f59e0b;
|
|
18
|
+
--hbs-color-helper-arg: #3b82f6;
|
|
19
|
+
--hbs-color-hash-key: #ec4899;
|
|
20
|
+
--hbs-color-hash-value: #3b82f6;
|
|
21
|
+
--hbs-color-literal: #16a34a;
|
|
22
|
+
--hbs-color-data-var: #f43f5e;
|
|
23
|
+
--hbs-color-subexpr-paren: #6b7280;
|
|
24
|
+
--hbs-color-comment: #9ca3af;
|
|
25
|
+
--hbs-color-raw: #f97316;
|
|
26
|
+
--hbs-color-brace: #6b7280;
|
|
27
|
+
--hbs-color-error: #ef4444;
|
|
28
|
+
--hbs-color-error-bg: rgba(239, 68, 68, 0.1);
|
|
29
|
+
|
|
30
|
+
/* Autocomplete colors */
|
|
31
|
+
--hbs-color-autocomplete-bg: #ffffff;
|
|
32
|
+
--hbs-color-autocomplete-border: #e5e7eb;
|
|
33
|
+
--hbs-color-autocomplete-selected: #f3f4f6;
|
|
34
|
+
--hbs-color-autocomplete-hover: #f9fafb;
|
|
35
|
+
|
|
36
|
+
/* Autocomplete dimensions */
|
|
37
|
+
--hbs-autocomplete-min-width: 180px;
|
|
38
|
+
--hbs-autocomplete-max-height: 192px;
|
|
39
|
+
|
|
40
|
+
/* Error bar colors */
|
|
41
|
+
--hbs-color-error-bar-bg: rgba(239, 68, 68, 0.1);
|
|
42
|
+
--hbs-color-error-bar-border: rgba(239, 68, 68, 0.5);
|
|
43
|
+
|
|
44
|
+
/* Spacing */
|
|
45
|
+
--hbs-padding: 12px;
|
|
46
|
+
--hbs-font-size: 14px;
|
|
47
|
+
--hbs-line-height: 1.5;
|
|
48
|
+
--hbs-font-family:
|
|
49
|
+
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
50
|
+
--hbs-border-radius: 6px;
|
|
51
|
+
--hbs-tab-size: 2;
|
|
52
|
+
|
|
53
|
+
position: relative;
|
|
54
|
+
flex: 1;
|
|
55
|
+
font-family: var(--hbs-font-family);
|
|
56
|
+
font-size: var(--hbs-font-size);
|
|
57
|
+
line-height: var(--hbs-line-height);
|
|
58
|
+
background-color: var(--hbs-color-background);
|
|
59
|
+
border-radius: var(--hbs-border-radius);
|
|
60
|
+
-webkit-text-size-adjust: 100%;
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Dark theme */
|
|
65
|
+
.hbs-editor.hbs-theme-dark {
|
|
66
|
+
--hbs-color-text: #f9fafb;
|
|
67
|
+
--hbs-color-background: #1f2937;
|
|
68
|
+
--hbs-color-border: #374151;
|
|
69
|
+
--hbs-color-caret: #f9fafb;
|
|
70
|
+
--hbs-color-placeholder: #6b7280;
|
|
71
|
+
--hbs-color-autocomplete-bg: #1f2937;
|
|
72
|
+
--hbs-color-autocomplete-border: #374151;
|
|
73
|
+
--hbs-color-autocomplete-selected: #374151;
|
|
74
|
+
--hbs-color-autocomplete-hover: #4b5563;
|
|
75
|
+
--hbs-color-autocomplete-text: #f9fafb;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.hbs-editor.hbs-theme-dark .hbs-autocomplete-item {
|
|
79
|
+
color: var(--hbs-color-autocomplete-text, #f9fafb);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Highlight layer */
|
|
83
|
+
.hbs-editor-highlight {
|
|
84
|
+
position: absolute;
|
|
85
|
+
inset: 0;
|
|
86
|
+
overflow: auto;
|
|
87
|
+
white-space: pre-wrap;
|
|
88
|
+
word-wrap: break-word;
|
|
89
|
+
overflow-wrap: break-word;
|
|
90
|
+
border-radius: var(--hbs-border-radius);
|
|
91
|
+
border: 1px solid transparent;
|
|
92
|
+
background: transparent;
|
|
93
|
+
padding: var(--hbs-padding);
|
|
94
|
+
pointer-events: none;
|
|
95
|
+
color: var(--hbs-color-text);
|
|
96
|
+
tab-size: var(--hbs-tab-size);
|
|
97
|
+
-moz-tab-size: var(--hbs-tab-size);
|
|
98
|
+
/* Hide scrollbars - scrolling is synced from textarea */
|
|
99
|
+
scrollbar-width: none;
|
|
100
|
+
-ms-overflow-style: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.hbs-editor-highlight::-webkit-scrollbar {
|
|
104
|
+
display: none;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.hbs-editor-highlight code {
|
|
108
|
+
font-family: inherit;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.hbs-editor.hbs-readonly .hbs-editor-highlight {
|
|
112
|
+
background: rgba(0, 0, 0, 0.03);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Textarea layer */
|
|
116
|
+
.hbs-editor-textarea {
|
|
117
|
+
position: absolute;
|
|
118
|
+
inset: 0;
|
|
119
|
+
resize: none;
|
|
120
|
+
border-radius: var(--hbs-border-radius);
|
|
121
|
+
border: 1px solid var(--hbs-color-border);
|
|
122
|
+
background: transparent;
|
|
123
|
+
padding: var(--hbs-padding);
|
|
124
|
+
font-family: inherit;
|
|
125
|
+
font-size: inherit;
|
|
126
|
+
line-height: inherit;
|
|
127
|
+
white-space: pre-wrap;
|
|
128
|
+
word-wrap: break-word;
|
|
129
|
+
overflow-wrap: break-word;
|
|
130
|
+
color: transparent;
|
|
131
|
+
caret-color: var(--hbs-color-caret);
|
|
132
|
+
outline: none;
|
|
133
|
+
tab-size: var(--hbs-tab-size);
|
|
134
|
+
-moz-tab-size: var(--hbs-tab-size);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.hbs-editor-textarea:focus {
|
|
138
|
+
border-color: var(--hbs-color-focus-ring);
|
|
139
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.hbs-editor.hbs-readonly .hbs-editor-textarea {
|
|
143
|
+
cursor: default;
|
|
144
|
+
background: rgba(0, 0, 0, 0.03);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Placeholder */
|
|
148
|
+
.hbs-editor-placeholder {
|
|
149
|
+
color: var(--hbs-color-placeholder);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Token styles */
|
|
153
|
+
.hbs-token-variable {
|
|
154
|
+
color: var(--hbs-color-variable);
|
|
155
|
+
}
|
|
156
|
+
.hbs-token-variable-path {
|
|
157
|
+
color: var(--hbs-color-variable-path);
|
|
158
|
+
}
|
|
159
|
+
.hbs-token-block-keyword {
|
|
160
|
+
color: var(--hbs-color-block-keyword);
|
|
161
|
+
font-weight: 500;
|
|
162
|
+
}
|
|
163
|
+
.hbs-token-block-param {
|
|
164
|
+
color: var(--hbs-color-block-param);
|
|
165
|
+
}
|
|
166
|
+
.hbs-token-helper {
|
|
167
|
+
color: var(--hbs-color-helper);
|
|
168
|
+
font-weight: 500;
|
|
169
|
+
}
|
|
170
|
+
.hbs-token-helper-arg {
|
|
171
|
+
color: var(--hbs-color-helper-arg);
|
|
172
|
+
}
|
|
173
|
+
.hbs-token-hash-key {
|
|
174
|
+
color: var(--hbs-color-hash-key);
|
|
175
|
+
}
|
|
176
|
+
.hbs-token-hash-value {
|
|
177
|
+
color: var(--hbs-color-hash-value);
|
|
178
|
+
}
|
|
179
|
+
.hbs-token-literal {
|
|
180
|
+
color: var(--hbs-color-literal);
|
|
181
|
+
}
|
|
182
|
+
.hbs-token-data-var {
|
|
183
|
+
color: var(--hbs-color-data-var);
|
|
184
|
+
}
|
|
185
|
+
.hbs-token-subexpr-paren {
|
|
186
|
+
color: var(--hbs-color-subexpr-paren);
|
|
187
|
+
font-weight: 500;
|
|
188
|
+
}
|
|
189
|
+
.hbs-token-comment {
|
|
190
|
+
color: var(--hbs-color-comment);
|
|
191
|
+
font-style: italic;
|
|
192
|
+
}
|
|
193
|
+
.hbs-token-raw {
|
|
194
|
+
color: var(--hbs-color-raw);
|
|
195
|
+
}
|
|
196
|
+
.hbs-token-brace {
|
|
197
|
+
color: var(--hbs-color-brace);
|
|
198
|
+
}
|
|
199
|
+
.hbs-token-error {
|
|
200
|
+
color: var(--hbs-color-error);
|
|
201
|
+
background: var(--hbs-color-error-bg);
|
|
202
|
+
text-decoration: wavy underline var(--hbs-color-error);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Autocomplete dropdown */
|
|
206
|
+
.hbs-autocomplete {
|
|
207
|
+
position: absolute;
|
|
208
|
+
z-index: 50;
|
|
209
|
+
min-width: var(--hbs-autocomplete-min-width);
|
|
210
|
+
overflow: auto;
|
|
211
|
+
border-radius: var(--hbs-border-radius);
|
|
212
|
+
border: 1px solid var(--hbs-color-autocomplete-border);
|
|
213
|
+
background: var(--hbs-color-autocomplete-bg);
|
|
214
|
+
padding: 4px;
|
|
215
|
+
box-shadow:
|
|
216
|
+
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
217
|
+
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.hbs-autocomplete-item {
|
|
221
|
+
display: block;
|
|
222
|
+
width: 100%;
|
|
223
|
+
text-align: left;
|
|
224
|
+
cursor: pointer;
|
|
225
|
+
border-radius: 4px;
|
|
226
|
+
padding: 6px 8px;
|
|
227
|
+
font-family: var(--hbs-font-family);
|
|
228
|
+
font-size: var(--hbs-font-size);
|
|
229
|
+
background: transparent;
|
|
230
|
+
border: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.hbs-autocomplete-item:hover {
|
|
234
|
+
background: var(--hbs-color-autocomplete-hover);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.hbs-autocomplete-item.hbs-selected {
|
|
238
|
+
background: var(--hbs-color-autocomplete-selected);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* Error bar */
|
|
242
|
+
.hbs-error-bar {
|
|
243
|
+
position: absolute;
|
|
244
|
+
right: 8px;
|
|
245
|
+
bottom: 8px;
|
|
246
|
+
left: 8px;
|
|
247
|
+
display: flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
gap: 8px;
|
|
250
|
+
border-radius: var(--hbs-border-radius);
|
|
251
|
+
border: 1px solid var(--hbs-color-error-bar-border);
|
|
252
|
+
background: var(--hbs-color-error-bar-bg);
|
|
253
|
+
padding: 8px 12px;
|
|
254
|
+
font-size: 14px;
|
|
255
|
+
color: var(--hbs-color-error);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.hbs-error-bar svg {
|
|
259
|
+
flex-shrink: 0;
|
|
260
|
+
width: 16px;
|
|
261
|
+
height: 16px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.hbs-error-bar span {
|
|
265
|
+
overflow: hidden;
|
|
266
|
+
text-overflow: ellipsis;
|
|
267
|
+
white-space: nowrap;
|
|
268
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "handlebars-editor-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A React component for editing Handlebars templates with syntax highlighting and autocomplete",
|
|
5
|
+
"author": "Dogukan Incesu",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./styles.css": "./dist/styles.css"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"*.css"
|
|
26
|
+
],
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=18.0.0",
|
|
29
|
+
"react-dom": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"handlebars": "^4.7.8"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "^2.3.13",
|
|
36
|
+
"@testing-library/jest-dom": "^6.0.0",
|
|
37
|
+
"@testing-library/react": "^14.0.0",
|
|
38
|
+
"@types/react": "^18.2.0",
|
|
39
|
+
"@types/react-dom": "^18.2.0",
|
|
40
|
+
"jsdom": "^24.0.0",
|
|
41
|
+
"react": "^18.2.0",
|
|
42
|
+
"react-dom": "^18.2.0",
|
|
43
|
+
"tsup": "^8.0.0",
|
|
44
|
+
"typescript": "^5.3.0",
|
|
45
|
+
"vitest": "^1.0.0"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"handlebars",
|
|
49
|
+
"editor",
|
|
50
|
+
"syntax-highlighting",
|
|
51
|
+
"react",
|
|
52
|
+
"autocomplete",
|
|
53
|
+
"template"
|
|
54
|
+
],
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "https://github.com/dogukani/handlebars-editor"
|
|
58
|
+
},
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/dogukani/handlebars-editor/issues"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://dogukani.github.io/handlebars-editor",
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsup",
|
|
65
|
+
"dev": "tsup --watch",
|
|
66
|
+
"test": "vitest",
|
|
67
|
+
"test:run": "vitest run",
|
|
68
|
+
"check-types": "tsc --noEmit",
|
|
69
|
+
"lint": "biome lint src/ tests/",
|
|
70
|
+
"format": "biome format --write src/ tests/",
|
|
71
|
+
"check": "biome check src/ tests/",
|
|
72
|
+
"example": "pnpm --dir example dev",
|
|
73
|
+
"example:dist": "pnpm --dir example dev:dist",
|
|
74
|
+
"example:build": "pnpm --dir example build"
|
|
75
|
+
}
|
|
76
|
+
}
|