vscode-front-matter
vscode-front-matter copied to clipboard
Enhancement: Advanced/Conditional behavior in snippets
Is your feature request related to a problem? Please describe.
As I started to fill out the snippets for my theme, I started to run into cases where I have duplicated snippets to account for limitations in the current model. I'll include a few examples here to demonstrate.
Example: Links
I have defined two snippets for the ref
and relref
Hugo shortcodes. They are nearly identical.
Permalink Definition
{
"title": "Permalink",
"description": "Insert a fully-qualified permalink to another page.",
"body": "{{< ref \"[[path]]\" />}}",
"fields": [
{
"name": "path",
"title": "Relative Page Path",
"description": "Specify the relative path to another page from this one.",
"type": "string",
"single": true,
"required": true
}
]
}
Relative Permalink Definition
{
"title": "Relative Permalink",
"description": "Insert a site-relative permalink to another page.",
"body": "{{< relref \"[[path]]\" />}}",
"fields": [
{
"name": "path",
"title": "Relative Page Path",
"description": "Specify the relative path to another page from this one.",
"type": "string",
"single": true,
"required": true
}
]
}
The only substantive difference between them is that the relative permalink's tag is relref
and the non-relative permalink's tag is ref
. Without a way to define any logic, only to pass variables (or empty strings where those variables could be, if undefined, I could do this, but I think the UX would be a little confusing:
Workaround Definition for Links
{
"title": "Combined Permalink",
"description": "Insert a permalink to another page on the site.",
"body": "{{< [[relative]]ref \"[[path]]\" />}}",
"fields": [
{
"name": "path",
"title": "Page Path",
"description": "Specify the path to another page.",
"type": "string",
"single": true,
"required": true
},
{
"name": "relative",
"title": "Use Relative Path",
"description": "Specify 'rel' to treat the input path as relative to this page.",
"type": "choice",
"choices": [
"",
"rel"
]
}
]
}
Unused Fields
I have some shortcodes which are relatively complex, and made moreso by the possibility of having inner content, but not requiring it. One particular problem is that anything defined in the snippet will be added to the eventual Markdown, even if the fields aren't used.
For example, this snippet defining an embed for an itch game:
Itch embed definition
{
"title": "Itch Embed",
"description": "Add an embedded widget to a project on itch.io",
"body": [
"{{< itchio id=\"[[id]]\"",
" square=\"[[square]]\"",
" linkback=\"[[linkback]]\"",
" dark=\"[[dark]]\"",
"/>}}"
],
"fields": [
{
"name": "id",
"title": "ID: Specify the ID for the project's embeddable iframe",
"type": "string",
"single": true,
"default": ""
},
{
"name": "square",
"title": "Should display as a square instead of a rectangle",
"type": "choice",
"choices": ["", "true", "false"],
"default": ""
},
{
"name": "linkback",
"title": "linkback",
"type": "choice",
"choices": ["", "true", "false"],
"default": ""
},
{
"name": "dark",
"title": "dark",
"type": "choice",
"choices": ["", "true", "false"],
"default": ""
}
]
}
Here, even if the user only supplies the mandatory value for id
, we still get:
{{< itchio id="ExampleID"
square=""
linkback=""
dark=""
/>}}
To work around this (and the limitation for not being able to specify boolean values), I've just set everything to default to false
, which means updating the config in tandem with the theme because instead of allowing the theme to handle default behavior internally, I'm reimplementing it in the snippet definition.
Describe the solution you'd like
I can think of a few different directions for this, chiefly extending the syntax and allowing for opt-in to scripted snippets.
Possibility: Extend Syntax
I think it might be possible to extend the existing syntax to support (simple) conditionals.
For example, in the case of the link definition:
Extended Syntax: if
{
"title": "Combined Permalink",
"description": "Insert a permalink to another page on the site.",
"body": "{{< [[if(relative)]]rel[[end]]ref \"[[path]]\" />}}",
"fields": [
{
"name": "path",
"title": "Page Path",
"description": "Specify the path to another page.",
"type": "string",
"single": true,
"required": true
},
{
"name": "relative",
"title": "Use Relative Path",
"description": "Specify whether to treat the input path as relative to this page.",
"type": "boolean",
}
]
}
I think there's definitely some concerns here with regard to overloading and over-complicating things. The further this gets from "array of strings to interpolate", the less useful it is to the majority of people I think. Too much cognitive load is definitely not the goal.
An alternative here would possibly be to allow maps in addition to strings in the body
array.
Extended Syntax: Maps in Body
{
"title": "Combined Permalink",
"description": "Insert a permalink to another page on the site.",
"body": [
{
"text": "{{<",
"after": " "
},
{
"if": {
"relative": true,
"text": "relref"
},
"else": {
"text": "ref"
},
"after": " "
},
"\"[[path]]\" />}}"
],
"fields": [
{
"name": "path",
"title": "Page Path",
"description": "Specify the path to another page.",
"type": "string",
"single": true,
"required": true
},
{
"name": "relative",
"title": "Use Relative Path",
"description": "Specify 'rel' to treat the input path as relative to this page.",
"type": "boolean"
}
]
}
This isn't thought out in detail, of course - just proposes a minimal idea for supporting if/else on the truthiness of a variable and controlling the spacing after the map entry (instead of always getting a newline).
Possibility: Scripted Snippets
I think this is likely the more coherent and functional longterm option, thought it requires at least as much thought around implementation and presents security concerns (in the same sense as any other scripts, I suppose).
This would look something like adding a script
key to the snippet definition and pointing at a script file (maybe limited to javascript initially) that the snippet arguments get passed to for building and returning the string.
For the itch embed, that might look like this:
Itch Embed Scripted Snippet JSON
{
"title": "Itch Embed",
"description": "Add an embedded widget to a project on itch.io",
"script": "./itch.js",
"fields": [
{
"name": "id",
"title": "ID: Specify the ID for the project's embeddable iframe",
"type": "boolean"
},
{
"name": "square",
"title": "Should display as a square instead of a rectangle",
"type": "boolean"
},
{
"name": "linkback",
"title": "linkback",
"type": "boolean"
},
{
"name": "dark",
"title": "dark",
"type": "boolean"
}
]
}
Itch Embed Scripted Snippet Script
Apologies for terrible code 😭
const arguments = process.argv;
if (arguments && arguments.length > 0) {
const fields = JSON.parse(arguments[2]); // The field data the user filled in
const fieldPrefix = "\n " // Newline/spacing for additinoal keys
var shortcode = `{{< itchio id="${fields.id}"`
var closeOnNewLine = false
if (fields.square) {
closeOnNewLine = true
shortcode += `${fieldPrefix}square=${fields.square}`
}
if (fields.linkback) {
closeOnNewLine = true
shortcode += `${fieldPrefix}linkback=${fields.linkback}`
}
if (fields.dark) {
closeOnNewLine = true
shortcode += `${fieldPrefix}dark=${fields.dark}`
}
if (closeOnNewLine) {
shortcode += "\n"
} else {
shortcode += ' '
}
shortcode += '>}}'
console.log(shortcode);
}
I think this would provide a lot of flexibility for those who need/want it but keep it scoped so there's still a really useful simplified option for people to use. This could also be a place to gate additional support for values, like boolean. If you need something more advanced than string/choice, you may well want to write a scripted snippet.
Describe alternatives you've considered
For now, I've been duplicating my snippets, explicitly setting defaults, and ignoring the unused values - a user can delete them or leave them in place.
Additional context
N/A
Thank you for providing so many details again @michaeltlombardi, and I see the need for it. Looking at the two solutions you provided, I think the scripted snippets come closest to what we already have. Plus, as you already mentioned, you can do more advanced things.
If I think about it for my website, a scripted snippet will allow me to insert an image and automatically generate the lqip (low quality image placeholder). At the moment, I got a custom action for this.
The only thing to think about is how to provide the arguments and in which structure/order.
For the arguments, is there anything we need to pass that isn't defined in the snippet fields? I think the only major difference in need from the existing structure is a way to specify the &
prefixed variables, like &selection
or &mediaURL
.
For passing the arguments, I think passing the JSON blob of the data itself is likely the easiest option. That makes this template functional for everyone:
const arguments = process.argv;
if (arguments && arguments.length > 0) {
const fields = JSON.parse(arguments[2]); // The field data the user filled in
var snippet = ""; // initialize the string of snippet text
// Your logic for building the snippet here
// Write the snippet text back to Front Matter for insertion
console.log(snippet);
}
And the PowerShell equivalent would be
if ($Args.length -gt 0) {
$Fields = ConvertFrom-Json $Args[0] # The field data the user filled in
$Snippet = '' # Initialize the string of snippet text
# Your logic for building the snippet here
# Write the snippet text back to Front Matter for insertion
$Snippet
}
I think trying to figure out how to order and pass the arguments as something other than a JSON blob is likely going to get a little complex and every language has pretty good methods for converting from a JSON blob into their native data structures (except bash, maybe? though in that case I would expect to use something like jq
to handle the data).
Probably a JSON string will be best indeed. In the script, you get the complete object as a sting, parse it, and are ready to go.