diff --git a/src/codemirror/fcTemplateLang/index.js b/src/codemirror/fcTemplateLang/index.js index 93e2bc6..d397375 100644 --- a/src/codemirror/fcTemplateLang/index.js +++ b/src/codemirror/fcTemplateLang/index.js @@ -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)] + }) + ]) } diff --git a/src/codemirror/fcTemplateLang/schema.js b/src/codemirror/fcTemplateLang/schema.js new file mode 100644 index 0000000..9db1dea --- /dev/null +++ b/src/codemirror/fcTemplateLang/schema.js @@ -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: () => '' +} diff --git a/src/components/Dashboard/common/CodeBlockEditor.jsx b/src/components/Dashboard/common/CodeBlockEditor.jsx index 25b8bef..b8ac8e2 100644 --- a/src/components/Dashboard/common/CodeBlockEditor.jsx +++ b/src/components/Dashboard/common/CodeBlockEditor.jsx @@ -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]) } diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx index ae60a27..be57872 100644 --- a/src/components/Dashboard/common/ObjectProperty.jsx +++ b/src/components/Dashboard/common/ObjectProperty.jsx @@ -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, diff --git a/src/components/Dashboard/common/TemplateEditor.jsx b/src/components/Dashboard/common/TemplateEditor.jsx index 80bdf84..c142ef5 100644 --- a/src/components/Dashboard/common/TemplateEditor.jsx +++ b/src/components/Dashboard/common/TemplateEditor.jsx @@ -92,6 +92,7 @@ const TemplateEditor = ({ language={'fcTemplateLang'} objectData={objectData} isEditing={isEditing} + autoCompleteObject={objectData?.testObject} />