mirror of
https://github.com/pret/pokefirered.git
synced 2026-04-25 23:40:13 -05:00
Dump wild_encounter data to C/JSON
This commit is contained in:
parent
079310c70b
commit
3f7c66703b
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -42,6 +42,7 @@ sound/**/*.bin
|
|||
sound/songs/midi/*.s
|
||||
src/*.s
|
||||
src/data/items.h
|
||||
src/data/wild_encounters.h
|
||||
tags
|
||||
tools/agbcc
|
||||
tools/binutils
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -45,6 +45,7 @@ ELF = $(ROM:.gba=.elf)
|
|||
MAP = $(ROM:.gba=.map)
|
||||
|
||||
C_SUBDIR = src
|
||||
DATA_C_SUBDIR = src/data
|
||||
ASM_SUBDIR = asm
|
||||
DATA_ASM_SUBDIR = data
|
||||
SONG_SUBDIR = sound/songs
|
||||
|
|
|
|||
|
|
@ -6,17 +6,6 @@
|
|||
.section .rodata
|
||||
.align 2
|
||||
|
||||
.include "data/wild_encounters.inc"
|
||||
|
||||
gUnknown_83CA71C:: @ 83CA71C
|
||||
.byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27
|
||||
.byte 2, 2, 2, 3, 3, 3, 7, 7, 7, 20, 20, 14
|
||||
.byte 13, 13, 13, 13, 18, 18, 18, 18, 8, 8, 4, 4
|
||||
.byte 15, 15, 11, 11, 9, 9, 17, 17, 17, 16, 16, 16
|
||||
.byte 24, 24, 19, 19, 6, 6, 6, 5, 5, 5, 10, 10
|
||||
.byte 21, 21, 21, 22, 22, 22, 23, 23, 12, 12, 1, 1
|
||||
.byte 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26
|
||||
|
||||
.incbin "baserom.gba", 0x3CA770, 0xE80
|
||||
|
||||
gUnknown_83CB5F0:: @ 83CB5F0
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,9 +1,15 @@
|
|||
# JSON files are run through jsonproc, which is a tool that converts JSON data to an output file
|
||||
# based on an Inja template. https://github.com/pantor/inja
|
||||
|
||||
AUTO_GEN_TARGETS += src/data/items.h
|
||||
AUTO_GEN_TARGETS += $(DATA_C_SUBDIR)/items.h
|
||||
|
||||
src/data/items.h: src/data/items.json src/data/items.json.txt
|
||||
$(DATA_C_SUBDIR)/items.h: $(DATA_C_SUBDIR)/items.json $(DATA_C_SUBDIR)/items.json.txt
|
||||
$(JSONPROC) $^ $@
|
||||
|
||||
$(C_BUILDDIR)/item.o: c_dep += src/data/items.h
|
||||
$(C_BUILDDIR)/item.o: c_dep += $(DATA_C_SUBDIR)/items.h
|
||||
|
||||
AUTO_GEN_TARGETS += $(DATA_C_SUBDIR)/wild_encounters.h
|
||||
$(DATA_C_SUBDIR)/wild_encounters.h: $(DATA_C_SUBDIR)/wild_encounters.json $(DATA_C_SUBDIR)/wild_encounters.json.txt
|
||||
$(JSONPROC) $^ $@
|
||||
|
||||
$(C_BUILDDIR)/wild_encounter.o: c_dep += $(DATA_C_SUBDIR)/wild_encounters.h
|
||||
|
|
|
|||
12625
src/data/wild_encounters.json
Normal file
12625
src/data/wild_encounters.json
Normal file
File diff suppressed because it is too large
Load Diff
90
src/data/wild_encounters.json.txt
Normal file
90
src/data/wild_encounters.json.txt
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
{{ doNotModifyHeader }}
|
||||
|
||||
## for wild_encounter_group in wild_encounter_groups
|
||||
{% if wild_encounter_group.for_maps %}
|
||||
## for wild_encounter_field in wild_encounter_group.fields
|
||||
{% if not existsIn(wild_encounter_field, "groups") %}
|
||||
## for encounter_rate in wild_encounter_field.encounter_rates
|
||||
{% if loop.index == 0 %}
|
||||
#define ENCOUNTER_CHANCE_{{ upper(wild_encounter_field.type) }}_SLOT_{{ loop.index }} {{ encounter_rate }} {% else %}#define ENCOUNTER_CHANCE_{{ upper(wild_encounter_field.type) }}_SLOT_{{ loop.index }} ENCOUNTER_CHANCE_{{ upper(wild_encounter_field.type) }}_SLOT_{{ subtract(loop.index, 1) }} + {{ encounter_rate }}{% endif %} {{ setVarInt(wild_encounter_field.type, loop.index) }}
|
||||
## endfor
|
||||
#define ENCOUNTER_CHANCE_{{ upper(wild_encounter_field.type) }}_TOTAL (ENCOUNTER_CHANCE_{{ upper(wild_encounter_field.type) }}_SLOT_{{ getVar(wild_encounter_field.type) }})
|
||||
{% else %}
|
||||
## for field_subgroup_key, field_subgroup_subarray in wild_encounter_field.groups
|
||||
## for field_subgroup_index in field_subgroup_subarray
|
||||
{% if loop.index == 0 %}
|
||||
#define ENCOUNTER_CHANCE_{{ upper(wild_encounter_field.type) }}_{{ upper(field_subgroup_key) }}_SLOT_{{ field_subgroup_index }} {{ at(wild_encounter_field.encounter_rates, field_subgroup_index) }} {% else %}#define ENCOUNTER_CHANCE_{{ upper(wild_encounter_field.type) }}_{{ upper(field_subgroup_key) }}_SLOT_{{ field_subgroup_index }} ENCOUNTER_CHANCE_{{ upper(wild_encounter_field.type) }}_{{ upper(field_subgroup_key) }}_SLOT_{{ getVar("previous_slot") }} + {{ at(wild_encounter_field.encounter_rates, field_subgroup_index) }}{% endif %}{{ setVarInt(concat(wild_encounter_field.type, field_subgroup_key), field_subgroup_index) }}{{ setVarInt("previous_slot", field_subgroup_index) }}
|
||||
## endfor
|
||||
#define ENCOUNTER_CHANCE_{{ upper(wild_encounter_field.type) }}_{{ upper(field_subgroup_key) }}_TOTAL (ENCOUNTER_CHANCE_{{ upper(wild_encounter_field.type) }}_{{ upper(field_subgroup_key) }}_SLOT_{{ getVar(concat(wild_encounter_field.type, field_subgroup_key)) }})
|
||||
## endfor
|
||||
{% endif %}
|
||||
## endfor
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
## for encounter in wild_encounter_group.encounters
|
||||
{% if existsIn(encounter, "land_mons") %}
|
||||
const struct WildPokemon {{ encounter.base_label }}_LandMons[] =
|
||||
{
|
||||
## for wild_mon in encounter.land_mons.mons
|
||||
{ {{ wild_mon.min_level }}, {{ wild_mon.max_level }}, {{ wild_mon.species }} },
|
||||
## endfor
|
||||
};
|
||||
|
||||
const struct WildPokemonInfo {{ encounter.base_label }}_LandMonsInfo = { {{encounter.land_mons.encounter_rate}}, {{ encounter.base_label }}_LandMons };
|
||||
{% endif %}
|
||||
{% if existsIn(encounter, "water_mons") %}
|
||||
const struct WildPokemon {{ encounter.base_label }}_WaterMons[] =
|
||||
{
|
||||
## for wild_mon in encounter.water_mons.mons
|
||||
{ {{ wild_mon.min_level }}, {{ wild_mon.max_level }}, {{ wild_mon.species }} },
|
||||
## endfor
|
||||
};
|
||||
|
||||
const struct WildPokemonInfo {{ encounter.base_label }}_WaterMonsInfo = { {{encounter.water_mons.encounter_rate}}, {{ encounter.base_label }}_WaterMons };
|
||||
{% endif %}
|
||||
{% if existsIn(encounter, "rock_smash_mons") %}
|
||||
const struct WildPokemon {{ encounter.base_label }}_RockSmashMons[] =
|
||||
{
|
||||
## for wild_mon in encounter.rock_smash_mons.mons
|
||||
{ {{ wild_mon.min_level }}, {{ wild_mon.max_level }}, {{ wild_mon.species }} },
|
||||
## endfor
|
||||
};
|
||||
|
||||
const struct WildPokemonInfo {{ encounter.base_label }}_RockSmashMonsInfo = { {{encounter.rock_smash_mons.encounter_rate}}, {{ encounter.base_label }}_RockSmashMons };
|
||||
{% endif %}
|
||||
{% if existsIn(encounter, "fishing_mons") %}
|
||||
const struct WildPokemon {{ encounter.base_label }}_FishingMons[] =
|
||||
{
|
||||
## for wild_mon in encounter.fishing_mons.mons
|
||||
{ {{ wild_mon.min_level }}, {{ wild_mon.max_level }}, {{ wild_mon.species }} },
|
||||
## endfor
|
||||
};
|
||||
|
||||
const struct WildPokemonInfo {{ encounter.base_label }}_FishingMonsInfo = { {{encounter.fishing_mons.encounter_rate}}, {{ encounter.base_label }}_FishingMons };
|
||||
{% endif %}
|
||||
## endfor
|
||||
|
||||
const struct WildPokemonHeader {{ wild_encounter_group.label }}[] =
|
||||
{
|
||||
## for encounter in wild_encounter_group.encounters
|
||||
{
|
||||
.mapGroup = {% if wild_encounter_group.for_maps %}MAP_GROUP({{ removePrefix(encounter.map, "MAP_") }}){% else %}0{% endif %},
|
||||
.mapNum = {% if wild_encounter_group.for_maps %}MAP_NUM({{ removePrefix(encounter.map, "MAP_") }}){% else %}{{ loop.index1 }}{% endif %},
|
||||
.landMonsInfo = {% if existsIn(encounter, "land_mons") %}&{{ encounter.base_label }}_LandMonsInfo{% else %}NULL{% endif %},
|
||||
.waterMonsInfo = {% if existsIn(encounter, "water_mons") %}&{{ encounter.base_label }}_WaterMonsInfo{% else %}NULL{% endif %},
|
||||
.rockSmashMonsInfo = {% if existsIn(encounter, "rock_smash_mons") %}&{{ encounter.base_label }}_RockSmashMonsInfo{% else %}NULL{% endif %},
|
||||
.fishingMonsInfo = {% if existsIn(encounter, "fishing_mons") %}&{{ encounter.base_label }}_FishingMonsInfo{% else %}NULL{% endif %},
|
||||
},
|
||||
## endfor
|
||||
{
|
||||
.mapGroup = MAP_GROUP(UNDEFINED),
|
||||
.mapNum = MAP_NUM(UNDEFINED),
|
||||
.landMonsInfo = NULL,
|
||||
.waterMonsInfo = NULL,
|
||||
.rockSmashMonsInfo = NULL,
|
||||
.fishingMonsInfo = NULL,
|
||||
},
|
||||
};
|
||||
## endfor
|
||||
|
|
@ -31,8 +31,6 @@ struct WildEncounterData
|
|||
static EWRAM_DATA struct WildEncounterData sWildEncounterData = {};
|
||||
static EWRAM_DATA bool8 sWildEncountersDisabled = FALSE;
|
||||
|
||||
extern const u8 gUnknown_83CA71C[][12];
|
||||
|
||||
static bool8 UnlockedTanobyOrAreNotInTanoby(void);
|
||||
static u32 GenerateUnownPersonalityByLetter(u8 letter);
|
||||
static bool8 IsWildLevelAllowedByRepel(u8 level);
|
||||
|
|
@ -43,6 +41,25 @@ static u8 IsLeadMonHoldingCleanseTag(void);
|
|||
static u16 WildEncounterRandom(void);
|
||||
static void AddToWildEncounterRateBuff(u8 encouterRate);
|
||||
|
||||
#include "data/wild_encounters.h"
|
||||
|
||||
static const u8 sUnownLetterSlots[][12] = {
|
||||
// A A A A A A A A A A A ?
|
||||
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27},
|
||||
// C C C D D D H H H U U O
|
||||
{ 2, 2, 2, 3, 3, 3, 7, 7, 7, 20, 20, 14},
|
||||
// N N N N S S S S I I E E
|
||||
{13, 13, 13, 13, 18, 18, 18, 18, 8, 8, 4, 4},
|
||||
// P P L L J J R R R Q Q Q
|
||||
{15, 15, 11, 11, 9, 9, 17, 17, 17, 16, 16, 16},
|
||||
// Y Y T T G G G F F F K K
|
||||
{24, 24, 19, 19, 6, 6, 6, 5, 5, 5, 10, 10},
|
||||
// V V V W W W X X M M B B
|
||||
{21, 21, 21, 22, 22, 22, 23, 23, 12, 12, 1, 1},
|
||||
// Z Z Z Z Z Z Z Z Z Z Z !
|
||||
{25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26},
|
||||
};
|
||||
|
||||
void DisableWildEncounters(bool8 state)
|
||||
{
|
||||
sWildEncountersDisabled = state;
|
||||
|
|
@ -209,7 +226,7 @@ static void GenerateWildMon(u16 species, u8 level, u8 slot)
|
|||
else
|
||||
{
|
||||
chamber = gSaveBlock1Ptr->location.mapNum - MAP_NUM(SEVEN_ISLAND_TANOBY_RUINS_MONEAN_CHAMBER);
|
||||
personality = GenerateUnownPersonalityByLetter(gUnknown_83CA71C[chamber][slot]);
|
||||
personality = GenerateUnownPersonalityByLetter(sUnownLetterSlots[chamber][slot]);
|
||||
CreateMon(&gEnemyParty[0], species, level, 32, TRUE, personality, FALSE, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -517,7 +517,7 @@ public:
|
|||
typedef const_pointer iterator;
|
||||
typedef const_pointer const_iterator;
|
||||
typedef std::reverse_iterator< const_iterator > reverse_iterator;
|
||||
typedef std::reverse_iterator< const_iterator > const_reverse_iterator;
|
||||
typedef std::reverse_iterator< const_iterator > const_reverse_iterator;
|
||||
|
||||
typedef std::size_t size_type;
|
||||
typedef std::ptrdiff_t difference_type;
|
||||
|
|
@ -1411,6 +1411,9 @@ enum class ElementNotation {
|
|||
Pointer
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Class for lexer configuration.
|
||||
*/
|
||||
struct LexerConfig {
|
||||
std::string statement_open {"{%"};
|
||||
std::string statement_close {"%}"};
|
||||
|
|
@ -1421,6 +1424,9 @@ struct LexerConfig {
|
|||
std::string comment_close {"#}"};
|
||||
std::string open_chars {"#{"};
|
||||
|
||||
bool trim_blocks {false};
|
||||
bool lstrip_blocks {false};
|
||||
|
||||
void update_open_chars() {
|
||||
open_chars = "";
|
||||
if (open_chars.find(line_statement[0]) == std::string::npos) {
|
||||
|
|
@ -1438,6 +1444,9 @@ struct LexerConfig {
|
|||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Class for parser configuration.
|
||||
*/
|
||||
struct ParserConfig {
|
||||
ElementNotation notation {ElementNotation::Dot};
|
||||
};
|
||||
|
|
@ -1450,10 +1459,13 @@ struct ParserConfig {
|
|||
#ifndef PANTOR_INJA_FUNCTION_STORAGE_HPP
|
||||
#define PANTOR_INJA_FUNCTION_STORAGE_HPP
|
||||
|
||||
#include <vector>
|
||||
|
||||
// #include "bytecode.hpp"
|
||||
#ifndef PANTOR_INJA_BYTECODE_HPP
|
||||
#define PANTOR_INJA_BYTECODE_HPP
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
|
@ -1464,7 +1476,7 @@ struct ParserConfig {
|
|||
|
||||
namespace inja {
|
||||
|
||||
using namespace nlohmann;
|
||||
using json = nlohmann::json;
|
||||
|
||||
|
||||
struct Bytecode {
|
||||
|
|
@ -1492,6 +1504,7 @@ struct Bytecode {
|
|||
GreaterEqual,
|
||||
Less,
|
||||
LessEqual,
|
||||
At,
|
||||
Different,
|
||||
DivisibleBy,
|
||||
Even,
|
||||
|
|
@ -1594,6 +1607,9 @@ using namespace nlohmann;
|
|||
using Arguments = std::vector<const json*>;
|
||||
using CallbackFunction = std::function<json(Arguments& args)>;
|
||||
|
||||
/*!
|
||||
* \brief Class for builtin functions and user-defined callbacks.
|
||||
*/
|
||||
class FunctionStorage {
|
||||
public:
|
||||
void add_builtin(nonstd::string_view name, unsigned int num_args, Bytecode::Op op) {
|
||||
|
|
@ -1658,6 +1674,9 @@ class FunctionStorage {
|
|||
#define PANTOR_INJA_PARSER_HPP
|
||||
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// #include "bytecode.hpp"
|
||||
|
||||
|
|
@ -1678,12 +1697,17 @@ class FunctionStorage {
|
|||
#ifndef PANTOR_INJA_TOKEN_HPP
|
||||
#define PANTOR_INJA_TOKEN_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
// #include "string_view.hpp"
|
||||
|
||||
|
||||
|
||||
namespace inja {
|
||||
|
||||
/*!
|
||||
* \brief Helper-class for the inja Parser.
|
||||
*/
|
||||
struct Token {
|
||||
enum class Kind {
|
||||
Text,
|
||||
|
|
@ -1737,13 +1761,17 @@ struct Token {
|
|||
|
||||
}
|
||||
|
||||
#endif // PANTOR_INJA_TOKEN_HPP
|
||||
#endif // PANTOR_INJA_TOKEN_HPP
|
||||
|
||||
// #include "utils.hpp"
|
||||
#ifndef PANTOR_INJA_UTILS_HPP
|
||||
#define PANTOR_INJA_UTILS_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
// #include "string_view.hpp"
|
||||
|
||||
|
|
@ -1755,11 +1783,22 @@ inline void inja_throw(const std::string& type, const std::string& message) {
|
|||
throw std::runtime_error("[inja.exception." + type + "] " + message);
|
||||
}
|
||||
|
||||
inline std::ifstream open_file_or_throw(const std::string& path) {
|
||||
std::ifstream file;
|
||||
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
|
||||
try {
|
||||
file.open(path);
|
||||
} catch(const std::ios_base::failure& e) {
|
||||
inja_throw("file_error", "failed accessing file at '" + path + "'");
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
namespace string_view {
|
||||
inline nonstd::string_view slice(nonstd::string_view view, size_t start, size_t end) {
|
||||
start = std::min(start, view.size());
|
||||
end = std::min(std::max(start, end), view.size());
|
||||
return view.substr(start, end - start); // StringRef(Data + Start, End - Start);
|
||||
return view.substr(start, end - start); // StringRef(Data + Start, End - Start);
|
||||
}
|
||||
|
||||
inline std::pair<nonstd::string_view, nonstd::string_view> split(nonstd::string_view view, char Separator) {
|
||||
|
|
@ -1783,6 +1822,9 @@ namespace string_view {
|
|||
|
||||
namespace inja {
|
||||
|
||||
/*!
|
||||
* \brief Class for lexing an inja Template.
|
||||
*/
|
||||
class Lexer {
|
||||
enum class State {
|
||||
Text,
|
||||
|
|
@ -1831,12 +1873,15 @@ class Lexer {
|
|||
|
||||
// try to match one of the opening sequences, and get the close
|
||||
nonstd::string_view open_str = m_in.substr(m_pos);
|
||||
bool must_lstrip = false;
|
||||
if (inja::string_view::starts_with(open_str, m_config.expression_open)) {
|
||||
m_state = State::ExpressionStart;
|
||||
} else if (inja::string_view::starts_with(open_str, m_config.statement_open)) {
|
||||
m_state = State::StatementStart;
|
||||
must_lstrip = m_config.lstrip_blocks;
|
||||
} else if (inja::string_view::starts_with(open_str, m_config.comment_open)) {
|
||||
m_state = State::CommentStart;
|
||||
must_lstrip = m_config.lstrip_blocks;
|
||||
} else if ((m_pos == 0 || m_in[m_pos - 1] == '\n') &&
|
||||
inja::string_view::starts_with(open_str, m_config.line_statement)) {
|
||||
m_state = State::LineStart;
|
||||
|
|
@ -1844,8 +1889,13 @@ class Lexer {
|
|||
m_pos += 1; // wasn't actually an opening sequence
|
||||
goto again;
|
||||
}
|
||||
if (m_pos == m_tok_start) goto again; // don't generate empty token
|
||||
return make_token(Token::Kind::Text);
|
||||
|
||||
nonstd::string_view text = string_view::slice(m_in, m_tok_start, m_pos);
|
||||
if (must_lstrip)
|
||||
text = clear_final_line_if_whitespace(text);
|
||||
|
||||
if (text.empty()) goto again; // don't generate empty token
|
||||
return Token(Token::Kind::Text, text);
|
||||
}
|
||||
case State::ExpressionStart: {
|
||||
m_state = State::ExpressionBody;
|
||||
|
|
@ -1872,7 +1922,7 @@ class Lexer {
|
|||
case State::LineBody:
|
||||
return scan_body("\n", Token::Kind::LineStatementClose);
|
||||
case State::StatementBody:
|
||||
return scan_body(m_config.statement_close, Token::Kind::StatementClose);
|
||||
return scan_body(m_config.statement_close, Token::Kind::StatementClose, m_config.trim_blocks);
|
||||
case State::CommentBody: {
|
||||
// fast-scan to comment close
|
||||
size_t end = m_in.substr(m_pos).find(m_config.comment_close);
|
||||
|
|
@ -1883,7 +1933,10 @@ class Lexer {
|
|||
// return the entire comment in the close token
|
||||
m_state = State::Text;
|
||||
m_pos += end + m_config.comment_close.size();
|
||||
return make_token(Token::Kind::CommentClose);
|
||||
Token tok = make_token(Token::Kind::CommentClose);
|
||||
if (m_config.trim_blocks)
|
||||
skip_newline();
|
||||
return tok;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1891,7 +1944,7 @@ class Lexer {
|
|||
const LexerConfig& get_config() const { return m_config; }
|
||||
|
||||
private:
|
||||
Token scan_body(nonstd::string_view close, Token::Kind closeKind) {
|
||||
Token scan_body(nonstd::string_view close, Token::Kind closeKind, bool trim = false) {
|
||||
again:
|
||||
// skip whitespace (except for \n as it might be a close)
|
||||
if (m_tok_start >= m_in.size()) return make_token(Token::Kind::Eof);
|
||||
|
|
@ -1905,7 +1958,10 @@ class Lexer {
|
|||
if (inja::string_view::starts_with(m_in.substr(m_tok_start), close)) {
|
||||
m_state = State::Text;
|
||||
m_pos = m_tok_start + close.size();
|
||||
return make_token(closeKind);
|
||||
Token tok = make_token(closeKind);
|
||||
if (trim)
|
||||
skip_newline();
|
||||
return tok;
|
||||
}
|
||||
|
||||
// skip \n
|
||||
|
|
@ -2026,6 +2082,34 @@ class Lexer {
|
|||
Token make_token(Token::Kind kind) const {
|
||||
return Token(kind, string_view::slice(m_in, m_tok_start, m_pos));
|
||||
}
|
||||
|
||||
void skip_newline() {
|
||||
if (m_pos < m_in.size()) {
|
||||
char ch = m_in[m_pos];
|
||||
if (ch == '\n')
|
||||
m_pos += 1;
|
||||
else if (ch == '\r') {
|
||||
m_pos += 1;
|
||||
if (m_pos < m_in.size() && m_in[m_pos] == '\n')
|
||||
m_pos += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static nonstd::string_view clear_final_line_if_whitespace(nonstd::string_view text)
|
||||
{
|
||||
nonstd::string_view result = text;
|
||||
while (!result.empty()) {
|
||||
char ch = result.back();
|
||||
if (ch == ' ' || ch == '\t')
|
||||
result.remove_suffix(1);
|
||||
else if (ch == '\n' || ch == '\r')
|
||||
break;
|
||||
else
|
||||
return text;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -2036,6 +2120,7 @@ class Lexer {
|
|||
#ifndef PANTOR_INJA_TEMPLATE_HPP
|
||||
#define PANTOR_INJA_TEMPLATE_HPP
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -2045,6 +2130,9 @@ class Lexer {
|
|||
|
||||
namespace inja {
|
||||
|
||||
/*!
|
||||
* \brief The main inja Template.
|
||||
*/
|
||||
struct Template {
|
||||
std::vector<Bytecode> bytecodes;
|
||||
std::string content;
|
||||
|
|
@ -2054,7 +2142,7 @@ using TemplateStorage = std::map<std::string, Template>;
|
|||
|
||||
}
|
||||
|
||||
#endif // PANTOR_INJA_TEMPLATE_HPP
|
||||
#endif // PANTOR_INJA_TEMPLATE_HPP
|
||||
|
||||
// #include "token.hpp"
|
||||
|
||||
|
|
@ -2068,6 +2156,7 @@ namespace inja {
|
|||
|
||||
class ParserStatic {
|
||||
ParserStatic() {
|
||||
functions.add_builtin("at", 2, Bytecode::Op::At);
|
||||
functions.add_builtin("default", 2, Bytecode::Op::Default);
|
||||
functions.add_builtin("divisibleBy", 2, Bytecode::Op::DivisibleBy);
|
||||
functions.add_builtin("even", 1, Bytecode::Op::Even);
|
||||
|
|
@ -2107,13 +2196,16 @@ class ParserStatic {
|
|||
FunctionStorage functions;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Class for parsing an inja Template.
|
||||
*/
|
||||
class Parser {
|
||||
public:
|
||||
explicit Parser(const ParserConfig& parser_config, const LexerConfig& lexer_config, TemplateStorage& included_templates): m_config(parser_config), m_lexer(lexer_config), m_included_templates(included_templates), m_static(ParserStatic::get_instance()) { }
|
||||
|
||||
bool parse_expression(Template& tmpl) {
|
||||
if (!parse_expression_and(tmpl)) return false;
|
||||
if (m_tok.kind != Token::Kind::Id || m_tok.text != "or") return true;
|
||||
if (m_tok.kind != Token::Kind::Id || m_tok.text != static_cast<decltype(m_tok.text)>("or")) return true;
|
||||
get_next_token();
|
||||
if (!parse_expression_and(tmpl)) return false;
|
||||
append_function(tmpl, Bytecode::Op::Or, 2);
|
||||
|
|
@ -2122,7 +2214,7 @@ class Parser {
|
|||
|
||||
bool parse_expression_and(Template& tmpl) {
|
||||
if (!parse_expression_not(tmpl)) return false;
|
||||
if (m_tok.kind != Token::Kind::Id || m_tok.text != "and") return true;
|
||||
if (m_tok.kind != Token::Kind::Id || m_tok.text != static_cast<decltype(m_tok.text)>("and")) return true;
|
||||
get_next_token();
|
||||
if (!parse_expression_not(tmpl)) return false;
|
||||
append_function(tmpl, Bytecode::Op::And, 2);
|
||||
|
|
@ -2130,7 +2222,7 @@ class Parser {
|
|||
}
|
||||
|
||||
bool parse_expression_not(Template& tmpl) {
|
||||
if (m_tok.kind == Token::Kind::Id && m_tok.text == "not") {
|
||||
if (m_tok.kind == Token::Kind::Id && m_tok.text == static_cast<decltype(m_tok.text)>("not")) {
|
||||
get_next_token();
|
||||
if (!parse_expression_not(tmpl)) return false;
|
||||
append_function(tmpl, Bytecode::Op::Not, 1);
|
||||
|
|
@ -2145,7 +2237,7 @@ class Parser {
|
|||
Bytecode::Op op;
|
||||
switch (m_tok.kind) {
|
||||
case Token::Kind::Id:
|
||||
if (m_tok.text == "in")
|
||||
if (m_tok.text == static_cast<decltype(m_tok.text)>("in"))
|
||||
op = Bytecode::Op::In;
|
||||
else
|
||||
return true;
|
||||
|
|
@ -2233,7 +2325,9 @@ class Parser {
|
|||
append_callback(tmpl, func_token.text, num_args);
|
||||
return true;
|
||||
}
|
||||
} else if (m_tok.text == "true" || m_tok.text == "false" || m_tok.text == "null") {
|
||||
} else if (m_tok.text == static_cast<decltype(m_tok.text)>("true") ||
|
||||
m_tok.text == static_cast<decltype(m_tok.text)>("false") ||
|
||||
m_tok.text == static_cast<decltype(m_tok.text)>("null")) {
|
||||
// true, false, null are json literals
|
||||
if (brace_level == 0 && bracket_level == 0) {
|
||||
json_first = m_tok.text;
|
||||
|
|
@ -2312,7 +2406,7 @@ class Parser {
|
|||
bool parse_statement(Template& tmpl, nonstd::string_view path) {
|
||||
if (m_tok.kind != Token::Kind::Id) return false;
|
||||
|
||||
if (m_tok.text == "if") {
|
||||
if (m_tok.text == static_cast<decltype(m_tok.text)>("if")) {
|
||||
get_next_token();
|
||||
|
||||
// evaluate expression
|
||||
|
|
@ -2323,7 +2417,7 @@ class Parser {
|
|||
|
||||
// conditional jump; destination will be filled in by else or endif
|
||||
tmpl.bytecodes.emplace_back(Bytecode::Op::ConditionalJump);
|
||||
} else if (m_tok.text == "endif") {
|
||||
} else if (m_tok.text == static_cast<decltype(m_tok.text)>("endif")) {
|
||||
if (m_if_stack.empty()) {
|
||||
inja_throw("parser_error", "endif without matching if");
|
||||
}
|
||||
|
|
@ -2342,7 +2436,7 @@ class Parser {
|
|||
|
||||
// pop if stack
|
||||
m_if_stack.pop_back();
|
||||
} else if (m_tok.text == "else") {
|
||||
} else if (m_tok.text == static_cast<decltype(m_tok.text)>("else")) {
|
||||
if (m_if_stack.empty())
|
||||
inja_throw("parser_error", "else without matching if");
|
||||
auto& if_data = m_if_stack.back();
|
||||
|
|
@ -2358,7 +2452,7 @@ class Parser {
|
|||
if_data.prev_cond_jump = std::numeric_limits<unsigned int>::max();
|
||||
|
||||
// chained else if
|
||||
if (m_tok.kind == Token::Kind::Id && m_tok.text == "if") {
|
||||
if (m_tok.kind == Token::Kind::Id && m_tok.text == static_cast<decltype(m_tok.text)>("if")) {
|
||||
get_next_token();
|
||||
|
||||
// evaluate expression
|
||||
|
|
@ -2370,7 +2464,7 @@ class Parser {
|
|||
// conditional jump; destination will be filled in by else or endif
|
||||
tmpl.bytecodes.emplace_back(Bytecode::Op::ConditionalJump);
|
||||
}
|
||||
} else if (m_tok.text == "for") {
|
||||
} else if (m_tok.text == static_cast<decltype(m_tok.text)>("for")) {
|
||||
get_next_token();
|
||||
|
||||
// options: for a in arr; for a, b in obj
|
||||
|
|
@ -2389,7 +2483,7 @@ class Parser {
|
|||
get_next_token();
|
||||
}
|
||||
|
||||
if (m_tok.kind != Token::Kind::Id || m_tok.text != "in")
|
||||
if (m_tok.kind != Token::Kind::Id || m_tok.text != static_cast<decltype(m_tok.text)>("in"))
|
||||
inja_throw("parser_error",
|
||||
"expected 'in', got '" + m_tok.describe() + "'");
|
||||
get_next_token();
|
||||
|
|
@ -2403,7 +2497,7 @@ class Parser {
|
|||
tmpl.bytecodes.back().value = key_token.text;
|
||||
}
|
||||
tmpl.bytecodes.back().str = static_cast<std::string>(value_token.text);
|
||||
} else if (m_tok.text == "endfor") {
|
||||
} else if (m_tok.text == static_cast<decltype(m_tok.text)>("endfor")) {
|
||||
get_next_token();
|
||||
if (m_loop_stack.empty()) {
|
||||
inja_throw("parser_error", "endfor without matching for");
|
||||
|
|
@ -2415,7 +2509,7 @@ class Parser {
|
|||
tmpl.bytecodes.emplace_back(Bytecode::Op::EndLoop);
|
||||
tmpl.bytecodes.back().args = m_loop_stack.back() + 1; // loop body
|
||||
m_loop_stack.pop_back();
|
||||
} else if (m_tok.text == "include") {
|
||||
} else if (m_tok.text == static_cast<decltype(m_tok.text)>("include")) {
|
||||
get_next_token();
|
||||
|
||||
if (m_tok.kind != Token::Kind::String) {
|
||||
|
|
@ -2431,8 +2525,10 @@ class Parser {
|
|||
}
|
||||
// sys::path::remove_dots(pathname, true, sys::path::Style::posix);
|
||||
|
||||
Template include_template = parse_template(pathname);
|
||||
m_included_templates.emplace(pathname, include_template);
|
||||
if (m_included_templates.find(pathname) == m_included_templates.end()) {
|
||||
Template include_template = parse_template(pathname);
|
||||
m_included_templates.emplace(pathname, include_template);
|
||||
}
|
||||
|
||||
// generate a reference bytecode
|
||||
tmpl.bytecodes.emplace_back(Bytecode::Op::Include, json(pathname), Bytecode::Flag::ValueImmediate);
|
||||
|
|
@ -2552,10 +2648,10 @@ class Parser {
|
|||
}
|
||||
|
||||
std::string load_file(nonstd::string_view filename) {
|
||||
std::ifstream file(static_cast<std::string>(filename));
|
||||
std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
return text;
|
||||
}
|
||||
std::ifstream file = open_file_or_throw(static_cast<std::string>(filename));
|
||||
std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
return text;
|
||||
}
|
||||
|
||||
private:
|
||||
const ParserConfig& m_config;
|
||||
|
|
@ -2605,6 +2701,7 @@ class Parser {
|
|||
#if __cplusplus < 201402L
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
|
|
@ -2655,6 +2752,9 @@ namespace stdinja = std;
|
|||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
|
|
@ -2679,6 +2779,9 @@ inline nonstd::string_view convert_dot_to_json_pointer(nonstd::string_view dot,
|
|||
return nonstd::string_view(out.data(), out.size());
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Class for rendering a Template with data.
|
||||
*/
|
||||
class Renderer {
|
||||
std::vector<const json*>& get_args(const Bytecode& bc) {
|
||||
m_tmp_args.clear();
|
||||
|
|
@ -2765,7 +2868,7 @@ class Renderer {
|
|||
LoopLevel& level = m_loop_stack.back();
|
||||
|
||||
if (level.loop_type == LoopLevel::Type::Array) {
|
||||
level.data[static_cast<std::string>(level.value_name)] = level.values.at(level.index); // *level.it;
|
||||
level.data[static_cast<std::string>(level.value_name)] = level.values.at(level.index); // *level.it;
|
||||
auto& loopData = level.data["loop"];
|
||||
loopData["index"] = level.index;
|
||||
loopData["index1"] = level.index + 1;
|
||||
|
|
@ -2787,8 +2890,8 @@ class Renderer {
|
|||
enum class Type { Map, Array };
|
||||
|
||||
Type loop_type;
|
||||
nonstd::string_view key_name; // variable name for keys
|
||||
nonstd::string_view value_name; // variable name for values
|
||||
nonstd::string_view key_name; // variable name for keys
|
||||
nonstd::string_view value_name; // variable name for values
|
||||
json data; // data with loop info added
|
||||
|
||||
json values; // values to iterate over
|
||||
|
|
@ -2800,8 +2903,8 @@ class Renderer {
|
|||
// loop over map
|
||||
using KeyValue = std::pair<nonstd::string_view, json*>;
|
||||
using MapValues = std::vector<KeyValue>;
|
||||
MapValues map_values; // values to iterate over
|
||||
MapValues::iterator map_it; // iterator over values
|
||||
MapValues map_values; // values to iterate over
|
||||
MapValues::iterator map_it; // iterator over values
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -2835,11 +2938,11 @@ class Renderer {
|
|||
}
|
||||
case Bytecode::Op::PrintValue: {
|
||||
const json& val = *get_args(bc)[0];
|
||||
if (val.is_string())
|
||||
if (val.is_string()) {
|
||||
os << val.get_ref<const std::string&>();
|
||||
else
|
||||
} else {
|
||||
os << val.dump();
|
||||
// val.dump(os);
|
||||
}
|
||||
pop_args(bc);
|
||||
break;
|
||||
}
|
||||
|
|
@ -2870,7 +2973,15 @@ class Renderer {
|
|||
break;
|
||||
}
|
||||
case Bytecode::Op::Length: {
|
||||
auto result = get_args(bc)[0]->size();
|
||||
const json& val = *get_args(bc)[0];
|
||||
|
||||
int result;
|
||||
if (val.is_string()) {
|
||||
result = val.get_ref<const std::string&>().length();
|
||||
} else {
|
||||
result = val.size();
|
||||
}
|
||||
|
||||
pop_args(bc);
|
||||
m_stack.emplace_back(result);
|
||||
break;
|
||||
|
|
@ -2882,6 +2993,13 @@ class Renderer {
|
|||
m_stack.emplace_back(std::move(result));
|
||||
break;
|
||||
}
|
||||
case Bytecode::Op::At: {
|
||||
auto args = get_args(bc);
|
||||
auto result = args[0]->at(args[1]->get<int>());
|
||||
pop_args(bc);
|
||||
m_stack.emplace_back(result);
|
||||
break;
|
||||
}
|
||||
case Bytecode::Op::First: {
|
||||
auto result = get_args(bc)[0]->front();
|
||||
pop_args(bc);
|
||||
|
|
@ -3091,7 +3209,7 @@ class Renderer {
|
|||
break;
|
||||
}
|
||||
case Bytecode::Op::Include:
|
||||
Renderer(m_included_templates, m_callbacks).render_to(os, m_included_templates.find(get_imm(bc)->get_ref<const std::string&>())->second, data);
|
||||
Renderer(m_included_templates, m_callbacks).render_to(os, m_included_templates.find(get_imm(bc)->get_ref<const std::string&>())->second, *m_data);
|
||||
break;
|
||||
case Bytecode::Op::Callback: {
|
||||
auto callback = m_callbacks.find_callback(bc.str, bc.args);
|
||||
|
|
@ -3216,12 +3334,17 @@ class Renderer {
|
|||
|
||||
// #include "template.hpp"
|
||||
|
||||
// #include "utils.hpp"
|
||||
|
||||
|
||||
|
||||
namespace inja {
|
||||
|
||||
using namespace nlohmann;
|
||||
|
||||
/*!
|
||||
* \brief Class for changing the configuration.
|
||||
*/
|
||||
class Environment {
|
||||
class Impl {
|
||||
public:
|
||||
|
|
@ -3238,7 +3361,7 @@ class Environment {
|
|||
std::unique_ptr<Impl> m_impl;
|
||||
|
||||
public:
|
||||
Environment(): Environment("./") { }
|
||||
Environment(): Environment("") { }
|
||||
|
||||
explicit Environment(const std::string& global_path): m_impl(stdinja::make_unique<Impl>()) {
|
||||
m_impl->input_path = global_path;
|
||||
|
|
@ -3277,6 +3400,16 @@ class Environment {
|
|||
m_impl->lexer_config.update_open_chars();
|
||||
}
|
||||
|
||||
/// Sets whether to remove the first newline after a block
|
||||
void set_trim_blocks(bool trim_blocks) {
|
||||
m_impl->lexer_config.trim_blocks = trim_blocks;
|
||||
}
|
||||
|
||||
/// Sets whether to strip the spaces and tabs from the start of a line to a block
|
||||
void set_lstrip_blocks(bool lstrip_blocks) {
|
||||
m_impl->lexer_config.lstrip_blocks = lstrip_blocks;
|
||||
}
|
||||
|
||||
/// Sets the element notation syntax
|
||||
void set_element_notation(ElementNotation notation) {
|
||||
m_impl->parser_config.notation = notation;
|
||||
|
|
@ -3290,8 +3423,8 @@ class Environment {
|
|||
|
||||
Template parse_template(const std::string& filename) {
|
||||
Parser parser(m_impl->parser_config, m_impl->lexer_config, m_impl->included_templates);
|
||||
return parser.parse_template(m_impl->input_path + static_cast<std::string>(filename));
|
||||
}
|
||||
return parser.parse_template(m_impl->input_path + static_cast<std::string>(filename));
|
||||
}
|
||||
|
||||
std::string render(nonstd::string_view input, const json& data) {
|
||||
return render(parse(input), data);
|
||||
|
|
@ -3304,35 +3437,35 @@ class Environment {
|
|||
}
|
||||
|
||||
std::string render_file(const std::string& filename, const json& data) {
|
||||
return render(parse_template(filename), data);
|
||||
}
|
||||
return render(parse_template(filename), data);
|
||||
}
|
||||
|
||||
std::string render_file_with_json_file(const std::string& filename, const std::string& filename_data) {
|
||||
const json data = load_json(filename_data);
|
||||
return render_file(filename, data);
|
||||
}
|
||||
const json data = load_json(filename_data);
|
||||
return render_file(filename, data);
|
||||
}
|
||||
|
||||
void write(const std::string& filename, const json& data, const std::string& filename_out) {
|
||||
std::ofstream file(m_impl->output_path + filename_out);
|
||||
file << render_file(filename, data);
|
||||
file.close();
|
||||
}
|
||||
std::ofstream file(m_impl->output_path + filename_out);
|
||||
file << render_file(filename, data);
|
||||
file.close();
|
||||
}
|
||||
|
||||
void write(const Template& temp, const json& data, const std::string& filename_out) {
|
||||
std::ofstream file(m_impl->output_path + filename_out);
|
||||
file << render(temp, data);
|
||||
file.close();
|
||||
}
|
||||
std::ofstream file(m_impl->output_path + filename_out);
|
||||
file << render(temp, data);
|
||||
file.close();
|
||||
}
|
||||
|
||||
void write_with_json_file(const std::string& filename, const std::string& filename_data, const std::string& filename_out) {
|
||||
const json data = load_json(filename_data);
|
||||
write(filename, data, filename_out);
|
||||
}
|
||||
void write_with_json_file(const std::string& filename, const std::string& filename_data, const std::string& filename_out) {
|
||||
const json data = load_json(filename_data);
|
||||
write(filename, data, filename_out);
|
||||
}
|
||||
|
||||
void write_with_json_file(const Template& temp, const std::string& filename_data, const std::string& filename_out) {
|
||||
const json data = load_json(filename_data);
|
||||
write(temp, data, filename_out);
|
||||
}
|
||||
void write_with_json_file(const Template& temp, const std::string& filename_data, const std::string& filename_out) {
|
||||
const json data = load_json(filename_data);
|
||||
write(temp, data, filename_out);
|
||||
}
|
||||
|
||||
std::ostream& render_to(std::ostream& os, const Template& tmpl, const json& data) {
|
||||
Renderer(m_impl->included_templates, m_impl->callbacks).render_to(os, tmpl, data);
|
||||
|
|
@ -3341,15 +3474,15 @@ class Environment {
|
|||
|
||||
std::string load_file(const std::string& filename) {
|
||||
Parser parser(m_impl->parser_config, m_impl->lexer_config, m_impl->included_templates);
|
||||
return parser.load_file(m_impl->input_path + filename);
|
||||
}
|
||||
return parser.load_file(m_impl->input_path + filename);
|
||||
}
|
||||
|
||||
json load_json(const std::string& filename) {
|
||||
std::ifstream file(m_impl->input_path + filename);
|
||||
json j;
|
||||
file >> j;
|
||||
return j;
|
||||
}
|
||||
std::ifstream file = open_file_or_throw(m_impl->input_path + filename);
|
||||
json j;
|
||||
file >> j;
|
||||
return j;
|
||||
}
|
||||
|
||||
void add_callback(const std::string& name, unsigned int numArgs, const CallbackFunction& callback) {
|
||||
m_impl->callbacks.add_callback(name, numArgs, callback);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
#include <map>
|
||||
|
||||
#include <string>
|
||||
using std::string;
|
||||
using std::string; using std::to_string;
|
||||
|
||||
#include <inja.hpp>
|
||||
using namespace inja;
|
||||
|
|
@ -36,7 +36,14 @@ int main(int argc, char *argv[])
|
|||
|
||||
// Add custom command callbacks.
|
||||
env.add_callback("doNotModifyHeader", 0, [jsonfilepath, templateFilepath](Arguments& args) {
|
||||
return "//\n// DO NOT MODIFY THIS FILE! IT IS AUTO-GENERATED FROM " + jsonfilepath +" and Inja template " + templateFilepath + "\n//\n";
|
||||
return "//\n// DO NOT MODIFY THIS FILE! It is auto-generated from " + jsonfilepath +" and Inja template " + templateFilepath + "\n//\n";
|
||||
});
|
||||
|
||||
env.add_callback("subtract", 2, [](Arguments& args) {
|
||||
int minuend = args.at(0)->get<int>();
|
||||
int subtrahend = args.at(1)->get<int>();
|
||||
|
||||
return minuend - subtrahend;
|
||||
});
|
||||
|
||||
env.add_callback("setVar", 2, [=](Arguments& args) {
|
||||
|
|
@ -46,6 +53,13 @@ int main(int argc, char *argv[])
|
|||
return "";
|
||||
});
|
||||
|
||||
env.add_callback("setVarInt", 2, [=](Arguments& args) {
|
||||
string key = args.at(0)->get<string>();
|
||||
string value = to_string(args.at(1)->get<int>());
|
||||
set_custom_var(key, value);
|
||||
return "";
|
||||
});
|
||||
|
||||
env.add_callback("getVar", 1, [=](Arguments& args) {
|
||||
string key = args.at(0)->get<string>();
|
||||
return get_custom_var(key);
|
||||
|
|
@ -67,7 +81,6 @@ int main(int argc, char *argv[])
|
|||
return rawValue.erase(0, prefix.length());
|
||||
});
|
||||
|
||||
// Add custom command callbacks.
|
||||
env.add_callback("removeSuffix", 2, [](Arguments& args) {
|
||||
string rawValue = args.at(0)->get<string>();
|
||||
string suffix = args.at(1)->get<string>();
|
||||
|
|
@ -78,6 +91,11 @@ int main(int argc, char *argv[])
|
|||
return rawValue.substr(0, i);
|
||||
});
|
||||
|
||||
// single argument is a json object
|
||||
env.add_callback("isEmpty", 1, [](Arguments& args) {
|
||||
return args.at(0)->empty();
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
env.write_with_json_file(templateFilepath, jsonfilepath, outputFilepath);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user