{ lib }: rec { # Whether the string is a reserved keyword in lua isKeyword = s: lib.elem s [ "and" "break" "do" "else" "elseif" "end" "false" "for" "function" "if" "in" "local" "nil" "not" "or" "repeat" "return" "then" "true" "until" "while" ]; # Valid lua identifiers are not reserved keywords, do not start with a digit, # and contain only letters, digits, and underscores. isIdentifier = s: !(isKeyword s) && (builtins.match "[A-Za-z_][0-9A-Za-z_]*" s) == [ ]; # Alias for nixpkgs lib's `mkLuaInline`, # but can also convert rawLua to lua-inline mkInline = v: lib.generators.mkLuaInline (v.__raw or v); # Whether the value is a lua-inline type isInline = v: v._type or null == "lua-inline"; /** Serialise a nix value as a lua object. Useful for defining your own plugins or structured config. # Type ``` toLuaObject :: Any -> String ``` */ toLuaObject = # toLua' with backwards-compatible options toLua' { }; # toLua' with default options, aliased as toLuaObject at the top-level toLua = toLua' { }; # Black functional magic that converts a bunch of different Nix types to their # lua equivalents! toLua' = { /** If this option is true, attrsets like { __pretty = fn; val = …; } will use fn to convert val to a lua representation. */ allowPrettyValues ? false, /** If this option is true, values like { __raw = "print('hi)"; } will render as print('hi') */ allowRawValues ? true, /** If this option is true, attrsets like { "__rawKey__print('hi')" = "a"; } will render as { [print('hi')] = "a"] } */ allowRawAttrKeys ? true, /** If this option is true, attrsets like { __unkeyed.1 = "a"; b = "b"; } will render as { "a", b = "b" } */ allowUnkeyedAttrs ? true, /** If this option is true, attrsets like { a = "a"; b = "b"; } will render as { a, b = "b" } instead of { ["a"] = "a", ["b"] = "b" } */ allowUnquotedAttrKeys ? true, # TODO: smartQuoteStyle: use ' or " based on string content # TODO: allowRawStrings: use [=[ ]=] strings for multiline strings /** If this option is true, attrsets like { a = null; b = ""; } will render as { ["b"] = "" } */ removeNullAttrValues ? true, /** If this option is true, attrsets like { a = { }; b = [ ]; c = ""; } will render as { ["c"] = "" } */ removeEmptyAttrValues ? true, /** If this option is true, lists like [ { } [ ] "" ] will render as { "" } */ removeEmptyListEntries ? false, /** If this option is true, lists like [ null "" ] will render as { "" } */ removeNullListEntries ? false, /** If this option is true, attrsets like { a.__empty = null; } will render as { ["a"] = { } }, ignoring other filters such as removeEmptyAttrValues. */ allowExplicitEmpty ? true, /** If this option is true, attrsets like { __emptyString = "foo"; } will render as { [""] = "foo" }. This is deprecated: use an attrset like { "" = "foo"; } instead. */ allowLegacyEmptyStringAttr ? true, /** If this option is true, the output is indented with newlines for attribute sets and lists */ # TODO: make this true by default multiline ? false, /** Initial indentation level */ indent ? "", }: let # If any of these are options are set, we need to preprocess the value needsPreprocessing = allowExplicitEmpty || removeEmptyAttrValues || removeNullAttrValues || removeEmptyListEntries || removeNullListEntries; # Slight optimization: only preprocess if we actually need to preprocessValue = value: if needsPreprocessing then removeEmptiesRecursive value else value; # Recursively filters `value`, removing any empty/null attrs (as configured) # Does not recurse into "special" attrs, such as `__raw` removeEmptiesRecursive = value: if allowPrettyValues && value ? __pretty && value ? val then value else if allowRawValues && value ? __raw then value else if isInline value then value else if lib.isDerivation value then value else if lib.isList value then let needsFiltering = removeNullListEntries || removeEmptyListEntries; fn = v: (removeNullListEntries -> (v != null)) && (removeEmptyListEntries -> (v != [ ] && v != { })); v' = map removeEmptiesRecursive value; in if needsFiltering then lib.filter fn v' else v' else if lib.isAttrs value then lib.concatMapAttrs ( n: v: let v' = removeEmptiesRecursive v; in if removeNullAttrValues && v == null then { } else if allowExplicitEmpty && v ? __empty then { ${n} = { }; } # Optimisation: check if v is empty before evaluating v' else if removeEmptyAttrValues && (v == [ ] || v == { }) then { } else if removeEmptyAttrValues && (v' == [ ] || v' == { }) then { } else { ${n} = v'; } ) value else value; # Return the dict-style table key, formatted as per the config toTableKey = s: if allowRawAttrKeys && lib.hasPrefix "__rawKey__" s then "[${lib.removePrefix "__rawKey__" s}]" else if allowUnquotedAttrKeys && isIdentifier s then s else if allowLegacyEmptyStringAttr && s == "__emptyString" then lib.trace ''nixvim(toLua): __emptyString is deprecated, just use an attribute named "".'' ( toTableKey "" ) else "[${go "" s}]"; # The main recursive function implementing `toLua`: # Visit a value and print it as lua, with the specified indentation. # Recursively visits child values with increasing indentation. go = indent: v: let # Calculate the start/end padding, including any linebreaks, # based on multiline config and current indentation. introSpace = if multiline then "\n${indent} " else " "; outroSpace = if multiline then "\n${indent}" else " "; in if v == null then "nil" else if lib.isInt v then toString v # toString loses precision on floats, so we use toJSON instead. # It can output an exponent form supported by lua. else if lib.isFloat v then builtins.toJSON v else if lib.isBool v then lib.boolToString v else if lib.isPath v || lib.isDerivation v then go indent "${v}" else if lib.isString v then # TODO: support lua's escape sequences, literal string, and content-appropriate quote style # See https://www.lua.org/pil/2.4.html # and https://www.lua.org/manual/5.1/manual.html#2.1 # and https://github.com/NixOS/nixpkgs/blob/00ba4c2c35f5e450f28e13e931994c730df05563/lib/generators.nix#L351-L365 builtins.toJSON v else if v == [ ] || v == { } then "{ }" else if lib.isFunction v then abort "nixvim(toLua): Unexpected function: " + lib.generators.toPretty { } v else if lib.isList v then "{" + introSpace + lib.concatMapStringsSep ("," + introSpace) (go (indent + " ")) v + outroSpace + "}" else if lib.isAttrs v then # apply pretty values if allowed if allowPrettyValues && v ? __pretty && v ? val then v.__pretty v.val # apply raw values if allowed else if allowRawValues && v ? __raw then # TODO: deprecate in favour of inline-lua v.__raw else if isInline v then # TODO: apply indentation to multiline raw values? "(${v.expr})" else "{" + introSpace + lib.concatStringsSep ("," + introSpace) ( lib.mapAttrsToList ( name: value: (if allowUnkeyedAttrs && lib.hasPrefix "__unkeyed" name then "" else toTableKey name + " = ") + lib.addErrorContext "while evaluating an attribute `${name}`" (go (indent + " ") value) ) v ) + outroSpace + "}" else abort "nixvim(toLua): should never happen (v = ${v})"; in value: go indent (preprocessValue value); }