Enhance fcTemplateLang with autocompletion support and XML schema integration

- Updated fcTemplateLang to accept an autoCompleteObject parameter, improving JavaScript autocompletion capabilities.
- Introduced a new schema.js file defining XML attributes and elements for FarmControl templates, enhancing template editing.
- Modified CodeBlockEditor and ObjectProperty components to pass autoCompleteObject, ensuring consistent autocompletion behavior.
- Updated TemplateEditor to utilize objectData for autocompletion, improving user experience in template editing.
This commit is contained in:
Tom Butcher 2026-08-10 03:33:01 +01:00
parent 24959ce9d8
commit eeccfe077f
5 changed files with 169 additions and 9 deletions

View File

@ -8,9 +8,22 @@ import {
} from '@codemirror/language'
import { styleTags, tags as t } from '@lezer/highlight'
import { parseMixed } from '@lezer/common'
import { javascriptLanguage } from '@codemirror/lang-javascript'
import { xmlLanguage } from '@codemirror/lang-xml'
import {
javascriptLanguage,
scopeCompletionSource,
localCompletionSource
} from '@codemirror/lang-javascript'
import {
xmlLanguage,
completeFromSchema,
autoCloseTags
} from '@codemirror/lang-xml'
import { parser as ejsParser } from './ejs.parser.js'
import {
fcTemplateElements,
fcTemplateAttributes,
fcTemplateHelpers
} from './schema.js'
/**
* FarmControl document template language: custom XML tags with EJS
@ -18,6 +31,7 @@ import { parser as ejsParser } from './ejs.parser.js'
* XML and JavaScript each get their own highlighting/support.
*
* @see https://codemirror.net/examples/mixed-language/
* @see https://codemirror.net/examples/autocompletion/
*/
const fcTemplateParser = ejsParser.configure({
props: [
@ -60,9 +74,42 @@ export const fcTemplateLanguage = LRLanguage.define({
}
})
function buildJsScope(autoCompleteObject) {
let data = autoCompleteObject
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch {
data = {}
}
}
if (!data || typeof data !== 'object' || Array.isArray(data)) {
data = {}
}
return {
...data,
fc: fcTemplateHelpers
}
}
/**
* Language support for FarmControl EJS+XML document templates.
* @param {{ autoCompleteObject?: object|string }} [options]
*/
export function fcTemplateLang() {
return new LanguageSupport(fcTemplateLanguage)
export function fcTemplateLang(options = {}) {
const { autoCompleteObject } = options
const jsScope = buildJsScope(autoCompleteObject)
return new LanguageSupport(fcTemplateLanguage, [
xmlLanguage.data.of({
autocomplete: completeFromSchema(
fcTemplateElements,
fcTemplateAttributes
)
}),
autoCloseTags,
javascriptLanguage.data.of({
autocomplete: [localCompletionSource, scopeCompletionSource(jsScope)]
})
])
}

View File

@ -0,0 +1,106 @@
/**
* XML schema for FarmControl document template tags.
* Mirrors custom elements / style attrs handled in farmcontrol-ws TemplateManager.
*/
const styleAttrs = [
'padding',
'width',
'height',
'maxWidth',
'maxHeight',
'gap',
'justify',
'align',
'border',
'borderRadius',
'vertical',
'grow',
'shrink',
'color',
'background',
'scale',
'textAlign',
'textSize',
'wordWrap'
]
const justifyValues = [
'flex-start',
'flex-end',
'center',
'space-between',
'space-around',
'space-evenly'
]
const alignValues = [
'flex-start',
'flex-end',
'center',
'stretch',
'baseline'
]
/** @type {import('@codemirror/lang-xml').AttrSpec[]} */
export const fcTemplateAttributes = [
...styleAttrs.map((name) => {
const attr = { name, global: true }
if (name === 'vertical') attr.values = ['true', 'false']
if (name === 'justify') attr.values = justifyValues
if (name === 'align') attr.values = alignValues
if (name === 'textAlign') {
attr.values = ['left', 'center', 'right', 'justify']
}
return attr
}),
{
name: 'format',
values: ['code128', 'code39', 'ean13', 'ean8', 'upc', 'qrcode']
},
{ name: 'barcodeWidth' },
{
name: 'type',
values: ['header', 'footer', 'body']
}
]
const allTags = [
'Flex',
'Container',
'Title1',
'Title2',
'Title3',
'Title4',
'Text',
'Bold',
'Divider',
'ProgressBar',
'Barcode',
'DateTime',
'Table',
'Row',
'Col'
]
/** @type {import('@codemirror/lang-xml').ElementSpec[]} */
export const fcTemplateElements = allTags.map((name) => {
const el = { name, top: true, children: allTags }
if (name === 'Barcode') {
el.attributes = ['format', 'barcodeWidth', ...styleAttrs]
} else if (name === 'Row') {
el.attributes = ['type', ...styleAttrs]
} else if (name === 'Divider') {
el.children = []
}
return el
})
/** Runtime helpers available as `fc` inside templates. */
export const fcTemplateHelpers = {
listObjects: async () => [],
getObject: async () => ({}),
formatDate: () => ''
}

View File

@ -32,7 +32,8 @@ export default function CodeBlockEditor({
height = 'auto',
showLineNumbers = true,
disabled = false,
minimal = false
minimal = false,
autoCompleteObject = null
}) {
const { isDarkMode } = useThemeContext()
const [codeMirrorOpen, setCodeMirrorOpen] = useState(false)
@ -57,7 +58,7 @@ export default function CodeBlockEditor({
return xml()
case 'fctemplatelang':
case 'ejs':
return fcTemplateLang()
return fcTemplateLang({ autoCompleteObject })
case 'html':
return html()
case 'css':
@ -86,7 +87,7 @@ export default function CodeBlockEditor({
default:
return javascript() // Default fallback
}
}, [language])
}, [language, autoCompleteObject])
const handleOnChange = (value) => {
if (typeof code == 'object' && language == 'json') {
@ -178,7 +179,7 @@ export default function CodeBlockEditor({
}
CodeBlockEditor.propTypes = {
code: PropTypes.string.isRequired,
code: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
language: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)
@ -190,5 +191,6 @@ CodeBlockEditor.propTypes = {
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
showLineNumbers: PropTypes.bool,
disabled: PropTypes.bool,
minimal: PropTypes.bool
minimal: PropTypes.bool,
autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string])
}

View File

@ -111,6 +111,7 @@ const ObjectProperty = ({
initial = false,
height = 'auto',
minimal = false,
autoCompleteObject = null,
previewOpen = false,
showPreview = true,
useFormItem = true,
@ -399,6 +400,7 @@ const ObjectProperty = ({
height={height}
readOnly={true}
minimal={minimal}
autoCompleteObject={autoCompleteObject}
/>
)
} else {
@ -906,6 +908,7 @@ const ObjectProperty = ({
language={language}
height={height}
minimal={minimal}
autoCompleteObject={autoCompleteObject}
{...inputProps}
/>
)
@ -1037,6 +1040,7 @@ ObjectProperty.propTypes = {
required: PropTypes.bool,
name: PropTypes.string,
language: PropTypes.string,
autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
min: PropTypes.number,

View File

@ -92,6 +92,7 @@ const TemplateEditor = ({
language={'fcTemplateLang'}
objectData={objectData}
isEditing={isEditing}
autoCompleteObject={objectData?.testObject}
/>
</div>
</Flex>