Skip to content

Commit 462b8bc

Browse files
authored
feat: Playground improvements (#9)
1 parent 2d5e469 commit 462b8bc

5 files changed

Lines changed: 199 additions & 4 deletions

File tree

src/FlexRender.Playground/FlexRender.Playground.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<ProjectReference Include="..\FlexRender.Yaml\FlexRender.Yaml.csproj" />
1919
<ProjectReference Include="..\FlexRender.Skia.Render\FlexRender.Skia.Render.csproj" />
2020
<ProjectReference Include="..\FlexRender.Content.Ndc\FlexRender.Content.Ndc.csproj" />
21+
<ProjectReference Include="..\FlexRender.QrCode.Skia.Render\FlexRender.QrCode.Skia.Render.csproj" />
22+
<ProjectReference Include="..\FlexRender.Barcode.Skia.Render\FlexRender.Barcode.Skia.Render.csproj" />
2123
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
2224
</ItemGroup>
2325

src/FlexRender.Playground/PlaygroundApi.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using FlexRender.Layout;
88
using FlexRender.Parsing;
99
using FlexRender.Parsing.Ast;
10+
using FlexRender.Barcode;
11+
using FlexRender.QrCode;
1012
using FlexRender.Rendering;
1113
using FlexRender.Skia;
1214
using FlexRender.Yaml;
@@ -42,7 +44,7 @@ public static void Initialize()
4244

4345
var builder = new FlexRenderBuilder()
4446
.WithNdc()
45-
.WithSkia();
47+
.WithSkia(skia => skia.WithQr().WithBarcode());
4648

4749
// Insert memory loader at highest priority so uploaded files win
4850
builder.ResourceLoaders.Insert(0, _memoryLoader);

src/FlexRender.Playground/wwwroot/main.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1135,7 +1135,15 @@ function render() {
11351135

11361136
switchToTab('preview');
11371137
} else {
1138-
statusText.textContent = 'Render returned empty \u2014 check console';
1138+
const lastError = api.GetLastError();
1139+
if (lastError) {
1140+
errorsPane.textContent = lastError;
1141+
statusBar.classList.add('error');
1142+
statusText.textContent = 'Error';
1143+
switchToTab('errors');
1144+
} else {
1145+
statusText.textContent = 'Render returned empty';
1146+
}
11391147
}
11401148
} catch (e) {
11411149
errorsPane.textContent = e.message || String(e);

src/FlexRender.Playground/wwwroot/schemas/flexrender-template.json

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -892,10 +892,89 @@
892892
"enum": ["ndc", "markdown", "html"]
893893
},
894894
"options": {
895-
"description": "Format-specific rendering options.",
896-
"type": "object"
895+
"description": "Format-specific rendering options. Properties depend on the chosen format.",
896+
"oneOf": [
897+
{ "$ref": "#/definitions/ndcOptions" },
898+
{ "type": "object", "description": "No options for this format.", "properties": {}, "additionalProperties": false }
899+
]
897900
}
898901
}
902+
},
903+
904+
"charsetStyle": {
905+
"description": "Per-charset style configuration for NDC content parser.",
906+
"type": "object",
907+
"properties": {
908+
"font": {
909+
"description": "Font registration name (e.g., \"bold\", \"default\").",
910+
"type": "string"
911+
},
912+
"font_family": {
913+
"description": "Explicit font family for this charset.",
914+
"type": "string"
915+
},
916+
"font_style": {
917+
"description": "Font style.",
918+
"type": "string",
919+
"enum": ["bold", "italic", "bold-italic"]
920+
},
921+
"font_size": {
922+
"description": "Explicit font size in pixels.",
923+
"type": "integer",
924+
"minimum": 1
925+
},
926+
"color": {
927+
"description": "Text color (hex format, e.g., \"#333\").",
928+
"type": "string"
929+
},
930+
"encoding": {
931+
"description": "Character encoding (e.g., \"none\", \"qwerty-jcuken\").",
932+
"type": "string"
933+
},
934+
"uppercase": {
935+
"description": "Convert text to uppercase.",
936+
"type": "boolean",
937+
"default": false
938+
}
939+
},
940+
"additionalProperties": false
941+
},
942+
943+
"ndcOptions": {
944+
"description": "Options for the NDC content parser.",
945+
"type": "object",
946+
"properties": {
947+
"input_encoding": {
948+
"description": "Byte encoding for binary data.",
949+
"type": "string",
950+
"enum": ["latin1", "iso-8859-1", "utf-8", "utf8", "ascii"],
951+
"default": "latin1"
952+
},
953+
"columns": {
954+
"description": "Max characters per line (receipt width).",
955+
"type": "integer",
956+
"minimum": 1,
957+
"default": 40
958+
},
959+
"font_family": {
960+
"description": "Font family for all text (e.g., \"JetBrains Mono\").",
961+
"type": "string"
962+
},
963+
"char_width_ratio": {
964+
"description": "Character width as fraction of font size for monospace fonts.",
965+
"type": "number",
966+
"minimum": 0.1,
967+
"default": 0.6
968+
},
969+
"charsets": {
970+
"description": "Per-charset style mappings keyed by designator character.",
971+
"type": "object",
972+
"additionalProperties": {
973+
"$ref": "#/definitions/charsetStyle"
974+
}
975+
}
976+
},
977+
"additionalProperties": false
899978
}
900979
}
901980
}

src/FlexRender.Playground/wwwroot/yaml-autocomplete.mjs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,17 @@ export function registerYamlAutocomplete(monaco, schema, options = {}) {
127127
}
128128
case 'template':
129129
return makeSuggestions(monaco, rootProps.template?.properties || {}, range, 'template');
130+
case 'content-options': {
131+
const formatDef = context.format
132+
? defs[context.format + 'Options']
133+
: null;
134+
const props = formatDef?.properties || {};
135+
return makeSuggestions(monaco, props, range, 'content-options');
136+
}
137+
case 'content-charset-item': {
138+
const props = defs.charsetStyle?.properties || {};
139+
return makeSuggestions(monaco, props, range, 'content-charset-item');
140+
}
130141
default:
131142
return { suggestions: [] };
132143
}
@@ -149,6 +160,30 @@ function detectContext(text, currentIndent) {
149160
if (lineIndent < currentIndent) {
150161
if (trimmed === 'canvas:') return { type: 'canvas' };
151162
if (trimmed === 'template:') return { type: 'template' };
163+
164+
// Check for charset designator (single word key, e.g. "I:")
165+
const isDesignatorKey = trimmed.match(/^\w+:$/);
166+
if (isDesignatorKey) {
167+
for (let k = i - 1; k >= 0; k--) {
168+
const prev = lines[k];
169+
const prevTrimmed = prev.trim();
170+
if (!prevTrimmed || prevTrimmed.startsWith('#')) continue;
171+
const prevIndent = prev.match(/^(\s*)/)[1].length;
172+
if (prevIndent < lineIndent) {
173+
if (prevTrimmed === 'charsets:') {
174+
return { type: 'content-charset-item' };
175+
}
176+
break;
177+
}
178+
}
179+
}
180+
181+
if (trimmed === 'options:') {
182+
const parentInfo = findContentParent(lines, i, lineIndent);
183+
if (parentInfo) {
184+
return { type: 'content-options', format: parentInfo.format };
185+
}
186+
}
152187
if (trimmed === 'fonts:' || trimmed === '- name:' || trimmed.startsWith('- name:')) {
153188
return { type: 'font-item' };
154189
}
@@ -190,6 +225,32 @@ function detectContext(text, currentIndent) {
190225
return { type: 'unknown' };
191226
}
192227

228+
function findContentParent(lines, fromIndex, optionsIndent) {
229+
let format = null;
230+
for (let i = fromIndex - 1; i >= 0; i--) {
231+
const line = lines[i];
232+
const trimmed = line.trim();
233+
if (!trimmed || trimmed.startsWith('#')) continue;
234+
const indent = line.match(/^(\s*)/)[1].length;
235+
if (indent >= optionsIndent) {
236+
const formatMatch = trimmed.match(/^format:\s*(\w+)/);
237+
if (formatMatch) format = formatMatch[1];
238+
continue;
239+
}
240+
const typeMatch = trimmed.match(/^-?\s*type:\s*(\w+)/);
241+
if (typeMatch) {
242+
if (typeMatch[1] === 'content') return { format };
243+
return null;
244+
}
245+
if (indent < optionsIndent) {
246+
const formatMatch = trimmed.match(/^-?\s*format:\s*(\w+)/);
247+
if (formatMatch) format = formatMatch[1];
248+
}
249+
if (indent === 0) break;
250+
}
251+
return null;
252+
}
253+
193254
function suggestValues(monaco, key, textUntilPosition, schema, defs, elementPropsMap, range, options) {
194255
const suggestions = [];
195256

@@ -312,6 +373,16 @@ function findPropertyDef(key, textUntilPosition, schema, defs, elementPropsMap)
312373
}
313374
}
314375

376+
// Check if inside content options or charset item
377+
const optionsContext = detectContentOptionsContext(lines);
378+
if (optionsContext === 'charset-item') {
379+
const charsetDef = defs.charsetStyle;
380+
if (charsetDef?.properties?.[cleanKey]) return charsetDef.properties[cleanKey];
381+
} else if (optionsContext) {
382+
const optDef = defs[optionsContext + 'Options'];
383+
if (optDef?.properties?.[cleanKey]) return optDef.properties[cleanKey];
384+
}
385+
315386
// Check flex item properties as fallback
316387
if (defs.flexItemProperties?.properties?.[cleanKey]) {
317388
return defs.flexItemProperties.properties[cleanKey];
@@ -320,6 +391,37 @@ function findPropertyDef(key, textUntilPosition, schema, defs, elementPropsMap)
320391
return null;
321392
}
322393

394+
function detectContentOptionsContext(lines) {
395+
for (let i = lines.length - 2; i >= 0; i--) {
396+
const line = lines[i];
397+
const trimmed = line.trim();
398+
if (!trimmed || trimmed.startsWith('#')) continue;
399+
const indent = line.match(/^(\s*)/)[1].length;
400+
401+
// Check for charset designator pattern
402+
if (trimmed.match(/^\w+:$/)) {
403+
for (let k = i - 1; k >= 0; k--) {
404+
const prev = lines[k];
405+
const prevTrimmed = prev.trim();
406+
if (!prevTrimmed || prevTrimmed.startsWith('#')) continue;
407+
const prevIndent = prev.match(/^(\s*)/)[1].length;
408+
if (prevIndent < indent && prevTrimmed === 'charsets:') {
409+
return 'charset-item';
410+
}
411+
if (prevIndent < indent) break;
412+
}
413+
}
414+
415+
if (trimmed === 'options:') {
416+
const parentInfo = findContentParent(lines, i, indent);
417+
return parentInfo?.format || null;
418+
}
419+
420+
if (indent === 0) break;
421+
}
422+
return null;
423+
}
424+
323425
function makeSuggestions(monaco, properties, range, context) {
324426
const suggestions = [];
325427
for (const [key, def] of Object.entries(properties)) {
@@ -367,6 +469,8 @@ function getSortOrder(key, context) {
367469
canvas: { width: '0', height: '1', background: '2', fixed: '3' },
368470
element: { type: '0', content: '1', children: '1', src: '1', data: '1', direction: '2', size: '2', color: '2', font: '3', padding: '4' },
369471
font: { name: '0', path: '1', fallback: '2' },
472+
'content-options': { input_encoding: '0', columns: '1', font_family: '2', char_width_ratio: '3', charsets: '4' },
473+
'content-charset-item': { font: '0', font_family: '1', font_style: '2', font_size: '3', color: '4', encoding: '5', uppercase: '6' },
370474
};
371475
return priority[context]?.[key] || '5';
372476
}

0 commit comments

Comments
 (0)