google / jsonnet

Jsonnet - The data templating language
http://jsonnet.org
Apache License 2.0
6.98k stars 440 forks source link

From a mustache template to jsonnet #979

Closed kiryph closed 2 years ago

kiryph commented 2 years ago

From Mustache to jsonnet

I have not yet too much experience with jsonnet. I am trying to convert an skhd configuration for yabai generated from a mustache template and yaml data file (about 900 lines) to a hopefully more compact jsonnet file.

My mustache file skhdrc.mustache is actually not that complicated and uses mustache variables, sections and inverted sections.

skhdrc.mustache ``` # Do not edit this file. Instead edit data.yaml & skhdrc.mustache, then run # $ mustache data.yaml skhdrc.mustache > skhdrc # to update this file. .blacklist [ "VirtualBoxVM" ] # ---------------------------------------------------- # skhd keybindings for the tiling window manager Yabai # ---------------------------------------------------- # https://github.com/koekeishiya/skhd # https://github.com/koekeishiya/yabai # Doc: $ man yabai {{#sections}} # -------------------- # {{^section_fullname}}{{section_name}}{{/section_fullname}}{{#section_fullname}}{{section_fullname}}{{/section_fullname}}{{#section_modifier}} {{section_modifier.symbol}}{{/section_modifier}} # -------------------- {{#shortcuts}} {{#shortcut_cmd}} # {{shortcut_name}} {{shortcut_modifier.symbol}} {{shortcut_key.symbol}} {{shortcut_modifier.skhd}} - {{shortcut_key.skhd}} : {{{shortcut_cmd}}} {{/shortcut_cmd}} {{/shortcuts}} {{/sections}} ```
small part of data.yaml ```yaml sections: - section_name: "Focus" section_modifier: &mod_focus <<: *shiftedctrl shortcuts: - shortcut_name: "Next" shortcut_modifier: *mod_focus shortcut_key: {symbol: "o", skhd: "o"} shortcut_cmd: | yabai -m window --focus next - shortcut_name: "Previous" shortcut_modifier: *mod_focus shortcut_key: {symbol: "i", skhd: "i"} shortcut_cmd: | yabai -m window --focus prev - shortcut_name: "Left" shortcut_modifier: *mod_focus shortcut_key: *west shortcut_cmd: | yabai -m window --focus west \ || yabai -m window --focus \ "$( (yabai -m query --spaces --display west || yabai -m query --spaces --display first) \ | jq -re '.[] | select(.visible == 1)."last-window"')" \ || yabai -m window last - shortcut_name: "Down" shortcut_fullname: "Down or Next" shortcut_modifier: *mod_focus shortcut_key: *south shortcut_cmd: | yabai -m window --focus south \ || yabai -m window --focus next \ || yabai -m window --focus \ "$( (yabai -m query --spaces --display next || yabai -m query --spaces --display first) \ | jq -re '.[] | select(.visible == 1)."first-window"')" \ || yabai -m window --focus first - shortcut_name: "Up" shortcut_fullname: "Up or Previous" shortcut_modifier: *mod_focus shortcut_key: *north shortcut_cmd: | yabai -m window --focus north \ || yabai -m window --focus prev \ || yabai -m window --focus \ "$( (yabai -m query --spaces --display prev || yabai -m query --spaces --display last) \ | jq -re '.[] | select(.visible == 1)."last-window"')" \ || yabai -m window --focus last ```

I expect an actual advantage of using jsonnet for the yaml file and not for the template file. However, I would like to remove the dependency to mustache when introducing jsonnet.

Before I dive in all the details of my specific situation, I wanted to figure out how to convert the simpler examples on https://mustache.github.io/mustache.5.html to sensible jsonnet constructs.

I have come up with following constructs

// From https://mustache.github.io/mustache.5.html to https://jsonnet.org

// ❯ jsonnet --version
// Jsonnet commandline interpreter v0.18.0
// ❯ jsonnet -S  -m . mustache.jsonnet

{
  local hash_synopsis = {
    name: 'Chris',
    value: 10000,
    taxed_value: 10000 - (10000 * 0.4),
    in_ca: true,
  },
  // MUSTACHE TEMPLATE:
  // Hello {{name}}
  // You have just won {{value}} dollars!
  // {{#in_ca}}
  // Well, {{taxed_value}} dollars, after taxes.
  // {{/in_ca}}
  'synopsis.txt': |||
    Hello %(name)s
    You have just won %(value)d dollars!
  ||| % hash_synopsis + if hash_synopsis.in_ca then
    'Well, %(taxed_value)d dollars, after taxes' % hash_synopsis,

  local hash_variables = {
    name: 'Chris',
    company: '<b>GitHub</b>',
  },
  // MUSTACHE TEMPLATE:
  // * {{name}}
  // * {{age}}
  // * {{company}}
  // * {{{company}}}
  'variables.txt': |||
    * %(name)s
    * %(age)s
    * %(company)s
    * %(company)s
  ||| % {
    name: hash_variables.name,
    age: if std.objectHas(hash_variables, 'age') then '%(age)d' % hash_variables else '',
    company: hash_variables.company,
  },
  // if there are no missing values, a more compact % hash_variables can be used.
  // Upcoming std.get(hash_variables, 'age', default='')
  // TODO missing escapeStringHTML similar to {{{ }}} or {{% name}} of mustache

  local hash_sections = {
    person: false,
  },
  // MUSTACHE TEMPLATE:
  // Shown.
  // {{#person}}
  //   Never shown!
  // {{/person}}
  'sections.txt': |||
    Shown.
  ||| + if hash_sections.person then
    'Never shown!' else '',

  local hash_non_empty_lists = {
    repo: [
      { name: 'resque' },
      { name: 'hub' },
      { name: 'rip' },
    ],
  },
  // MUSTACHE TEMPLATE:
  // {{#repo}}
  //   <b>{{name}}</b>
  // {{/repo}}
  'non-empty_lists.txt':
    std.lines([
      '<b>' + x.name + '</b>'
      for x in hash_non_empty_lists.repo
    ]),

  local hash_non_false_values = {
    'person?': { name: 'Jon' },
  },
  // MUSTACHE TEMPLATE:
  // {{#person?}}
  //   Hi {{name}}!
  // {{/person?}}
  'non-false_values.txt': if hash_non_false_values['person?'] != null then
    'Hi %(name)s!' % hash_non_false_values['person?']
  else
    '',

  local hash_inverted_sections = {
    repo: [],
  },
  // MUSTACHE TEMPLATE:
  // {{#repo}}
  //   <b>{{name}}</b>
  // {{/repo}}
  // {{^repo}}
  //   No repos :(
  // {{/repo}}
  'inverted-sections.txt':
    std.lines([
      '<b>' + x.name + '</b>'
      for x in hash_inverted_sections.repo
    ]) + if std.length(hash_inverted_sections.repo) == 0 then
      'No repos :(',
}

Most constructs are longer or syntactically more complicated and an std.escapeStringHTML does not exist in jsonnet. However, this is not important for me as long as the actual data part becomes shorter. The bulky data.yaml file is the motivation to switch to jsonnet and not the template part. But this does not mean that I do not want to write an elegant jsonnet code for the file generation.

Can someone else have a look at this and point out improvements of the jsonnet constructs if applicable or share their thoughts on this? Thank you.

sparkprime commented 2 years ago

Here's some food for thought. You might not like these and ultimately it's up to the programmer how to write it :) I will say that Jsonnet has weaker features for string templating as it's not purely a string templating language and is instead designed for templating structural data. So keeping the string templating part as simple as possible and doing more with structural data would be best... But it seems like that is what you're already doing.

How hard is it to escape HTML? We could add this if it's not too hard.

--- mustache-old.jsonnet 2022-02-13 12:38:01.556144444 +0000 +++ mustache.jsonnet 2022-02-13 12:56:41.844657415 +0000 @@ -12,11 +12,14 @@ // {{#in_ca}} // Well, {{taxed_value}} dollars, after taxes. // {{/in_ca}}

On Sun, 13 Feb 2022 at 11:00, kiryph @.***> wrote:

From Mustache https://mustache.github.io/mustache.5.html to jsonnet

I have not yet too much experience with jsonnet. I am trying to convert an skhd https://github.com/koekeishiya/skhd configuration for yabai https://github.com/koekeishiya/yabai generated from a mustache https://mustache.github.io/mustache.5.html template and yaml data file (about 900 lines) to a hopefully more compact jsonnet file.

My mustache file skhdrc.mustache is actually not that complicated and uses mustache variables, sections and inverted sections. skhdrc.mustache

Do not edit this file. Instead edit data.yaml & skhdrc.mustache, then run

$ mustache data.yaml skhdrc.mustache > skhdrc

to update this file.

.blacklist [

"VirtualBoxVM"

]

----------------------------------------------------

skhd keybindings for the tiling window manager Yabai

----------------------------------------------------

https://github.com/koekeishiya/skhd

https://github.com/koekeishiya/yabai

Doc: $ man yabai

{{#sections}}

--------------------

{{^section_fullname}}{{section_name}}{{/section_fullname}}{{#section_fullname}}{{section_fullname}}{{/section_fullname}}{{#section_modifier}} {{section_modifier.symbol}}{{/section_modifier}}

--------------------

{{#shortcuts}}

{{#shortcut_cmd}}

{{shortcut_name}} {{shortcut_modifier.symbol}} {{shortcut_key.symbol}}

{{shortcut_modifier.skhd}} - {{shortcut_key.skhd}} : {{{shortcut_cmd}}}

{{/shortcut_cmd}}

{{/shortcuts}}

{{/sections}}

small part of data.yaml

sections:

  • section_name: "Focus"

    section_modifier: &mod_focus

    <<: *shiftedctrl

    shortcuts:

    • shortcut_name: "Next"

      shortcut_modifier: *mod_focus

      shortcut_key: {symbol: "o", skhd: "o"}

      shortcut_cmd: | yabai -m window --focus next

    • shortcut_name: "Previous"

      shortcut_modifier: *mod_focus

      shortcut_key: {symbol: "i", skhd: "i"}

      shortcut_cmd: | yabai -m window --focus prev

    • shortcut_name: "Left"

      shortcut_modifier: *mod_focus

      shortcut_key: *west

      shortcut_cmd: | yabai -m window --focus west \ || yabai -m window --focus \ "$( (yabai -m query --spaces --display west || yabai -m query --spaces --display first) \ | jq -re '.[] | select(.visible == 1)."last-window"')" \ || yabai -m window last

    • shortcut_name: "Down"

      shortcut_fullname: "Down or Next"

      shortcut_modifier: *mod_focus

      shortcut_key: *south

      shortcut_cmd: | yabai -m window --focus south \ || yabai -m window --focus next \ || yabai -m window --focus \ "$( (yabai -m query --spaces --display next || yabai -m query --spaces --display first) \ | jq -re '.[] | select(.visible == 1)."first-window"')" \ || yabai -m window --focus first

    • shortcut_name: "Up"

      shortcut_fullname: "Up or Previous"

      shortcut_modifier: *mod_focus

      shortcut_key: *north

      shortcut_cmd: | yabai -m window --focus north \ || yabai -m window --focus prev \ || yabai -m window --focus \ "$( (yabai -m query --spaces --display prev || yabai -m query --spaces --display last) \ | jq -re '.[] | select(.visible == 1)."last-window"')" \ || yabai -m window --focus last

I expect an actual advantage of using jsonnet for the yaml file and not for the template file. However, I would like to remove the dependency to mustache when introducing jsonnet.

Before I dive in all the details of my specific situation, I wanted to figure out how to convert the simpler examples on https://mustache.github.io/mustache.5.html to sensible jsonnet constructs.

I have come up with following constructs

// From https://mustache.github.io/mustache.5.html to https://jsonnet.org

// ❯ jsonnet --version // Jsonnet commandline interpreter v0.18.0 // ❯ jsonnet -S -m . mustache.jsonnet

{

local hash_synopsis = {

name: 'Chris',

value: 10000,

taxed_value: 10000 - (10000 * 0.4),

in_ca: true,

},

// MUSTACHE TEMPLATE:

// Hello {{name}}

// You have just won {{value}} dollars!

// {{#in_ca}}

// Well, {{taxed_value}} dollars, after taxes.

// {{/in_ca}}

'synopsis.txt': ||| Hello %(name)s You have just won %(value)d dollars! ||| % hash_synopsis + if hash_synopsis.in_ca then

'Well, %(taxed_value)d dollars, after taxes' % hash_synopsis,

local hash_variables = {

name: 'Chris',

company: '<b>GitHub</b>',

},

// MUSTACHE TEMPLATE:

// * {{name}}

// * {{age}}

// * {{company}}

// * {{{company}}}

'variables.txt': |||

  • %(name)s
  • %(age)s
  • %(company)s
  • %(company)s ||| % {

    name: hash_variables.name,

    age: if std.objectHas(hash_variables, 'age') then '%(age)d' % hash_variables else '',

    company: hash_variables.company,

    },

    // if there are no missing values, a more compact % hash_variables can be used.

    // Upcoming std.get(hash_variables, 'age', default='')

    // TODO missing escapeStringHTML similar to {{{ }}} or {{% name}} of mustache

    local hash_sections = {

    person: false,

    },

    // MUSTACHE TEMPLATE:

    // Shown.

    // {{#person}}

    // Never shown!

    // {{/person}}

    'sections.txt': ||| Shown. ||| + if hash_sections.person then

    'Never shown!' else '',

    local hash_non_empty_lists = {

    repo: [

    { name: 'resque' },

    { name: 'hub' },

    { name: 'rip' },

    ],

    },

    // MUSTACHE TEMPLATE:

    // {{#repo}}

    // {{name}}

    // {{/repo}}

    'non-empty_lists.txt':

    std.lines([

    '' + x.name + ''

    for x in hash_non_empty_lists.repo

    ]),

    local hash_non_false_values = {

    'person?': { name: 'Jon' },

    },

    // MUSTACHE TEMPLATE:

    // {{#person?}}

    // Hi {{name}}!

    // {{/person?}}

    'non-false_values.txt': if hash_non_false_values['person?'] != null then

    'Hi %(name)s!' % hash_non_false_values['person?']

    else

    '',

    local hash_inverted_sections = {

    repo: [],

    },

    // MUSTACHE TEMPLATE:

    // {{#repo}}

    // {{name}}

    // {{/repo}}

    // {{^repo}}

    // No repos :(

    // {{/repo}}

    'inverted-sections.txt':

    std.lines([

    '' + x.name + ''

    for x in hash_inverted_sections.repo

    ]) + if std.length(hash_inverted_sections.repo) == 0 then

    'No repos :(',

}

Most constructs are longer or syntactically more complicated and an std.escapeStringHTML does not exist in jsonnet. However, this is not important for me as long as the actual data part becomes shorter. The bulky data.yaml file is the motivation to switch to jsonnet and not the template part. But this does not mean that I do not want to write an elegant jsonnet code for the file generation.

Can someone else have a look at this and point out improvements of the jsonnet constructs if applicable or share their thoughts on this? Thank you.

— Reply to this email directly, view it on GitHub https://github.com/google/jsonnet/issues/979, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABJBXXVYKIES7LCFXNMAGTU26FL7ANCNFSM5OIX5NOQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

You are receiving this because you are subscribed to this thread.Message ID: @.***>

kiryph commented 2 years ago

@sparkprime Thanks for your thoughts. Much appreciated.

You might not like these and ultimately it's up to the programmer how to write it :)

In the end it does not matter what I might like but what is practical and fits into the design rationale of jsonnet. It is better to facilitate its strengths and should not struggle with its weaknesses (just to remove one dependency).

I will say that Jsonnet has weaker features for string templating as it's not purely a string templating language and is instead designed for templating structural data. So keeping the string templating part as simple as possible and doing more with structural data would be best... But it seems like that is what you're already doing.

Yes. I now feel like I should accept adding jsonnet for the overboarding yaml file and still use mustache templating for the skhdrc file (and actually two further more).

How hard is it to escape HTML? We could add this if it's not too hard.

I cannot say. However, this is not part of this issue and I do not miss it. And also since jsonnet is not targeting unsatisfied mustache user or more generally html templating engines, I would assume this is not important for jsonnet.

There are many html templating engines (pug, handlebars, haml, liquid, ...) which are more feature rich than mustache and should be considered first.

If someone finds a real-world reason for jsonnet to have a std.escapeStringHTML, he/she should open an issue for this.