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 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
+ [![npm version](https://img.shields.io/npm/v/handlebars-editor-react.svg)](https://www.npmjs.com/package/handlebars-editor-react)
4
+ [![license](https://img.shields.io/npm/l/handlebars-editor-react.svg)](https://github.com/dogukani/handlebars-editor/blob/main/LICENSE)
5
+ [![downloads](https://img.shields.io/npm/dm/handlebars-editor-react.svg)](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;
@@ -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 };
@@ -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};
@@ -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
+ }