diff --git a/data/magic.mse-game/game b/data/magic.mse-game/game index 63aec8a9..7b266009 100644 --- a/data/magic.mse-game/game +++ b/data/magic.mse-game/game @@ -152,15 +152,15 @@ init script: }; # replaces — correctly - alternative_cost := replace_rule(match:"—|\\.", replace:"") + replace_rule(match:"[A-Z]", in_context:"", replace: { to_lower() }) - mana_cost := replace_rule(match:" ", replace: "") + alternative_cost := replace_rule(match:"\\.$", replace:"") + replace_rule(match:"^[A-Z]", replace: { to_lower() }) add := "" # default is nothing - format_cost := { - if substring(begin: 0, end: 13)=="—" then - alternative_cost() - else add + mana_cost() - }; - format_cost_start := replace_rule(match:"^()?[ ]?-", replace:"\\1—") + for_mana_costs := format_cost := { + if input.separator=="—" then + "{alternative_cost(input.param)}" + else + "{add}{input.param}" + } + long_dash := replace_rule(match:"-", replace:"—") # Utilities for keywords has_cc := { card.casting_cost != "" } has_pt := { card.pt != "" } @@ -237,10 +237,6 @@ init script: replace_rule( match: "AE", replace: "Æ" ) + - # step 6c : "s" remover for keywords - replace_rule( - match: "s" - replace: "" ) + # step 7 : italic reminder text replace_rule( match: "[(][^)\n]*[)]?", @@ -1125,23 +1121,22 @@ keyword parameter type: keyword parameter type: name: cost match: [ ][STXYZ0-9WUBRG/|]*|[-—][^(\n]* + separator before is: [ —-] + optional: false + # note: the separator is part of match refer script: name: normal - description: standard cost - script: \{{input}\} - refer script: - name: add nothing for mana costs description: When using mana only costs, doesn't include anything extra in the reminder text - script: \{format_cost({input})\} + script: \{{input}\} refer script: name: add "pay an additional " for mana costs description: When using mana only costs, words the reminder text as "pay an additional " - script: \{format_cost(add:"pay an additional ",{input})\} + script: \{for_mana_costs(add:"pay an additional ",{input})\} refer script: name: add "pay " for mana costs description: When using mana only costs, words the reminder text as "pay " - script: \{format_cost(add:"pay ",{input})\} - script: format_cost_start() + script: \{for_mana_costs(add:"pay ",{input})\} + separator script: long_dash() keyword parameter type: name: number match: [XYZ0-9]+ @@ -1204,9 +1199,9 @@ keyword: reminder: Target a {param1} as you play this. This card comes into play attached to that {param1}. keyword: keyword: Cycling - match: Cyclingcost + match: Cycling cost mode: expert - reminder: {format_cost(param1)}, Discard this card: Draw a card. + reminder: {param1}, Discard this card: Draw a card. keyword: keyword: Trample match: Trample @@ -1239,9 +1234,9 @@ keyword: reminder: This creature can’t attack. keyword: keyword: Cumulative upkeep - match: Cumulative upkeepcost + match: Cumulative upkeep cost mode: old - reminder: At the beginning of your upkeep, put an age counter on this permanent, then sacrifice it unless you {format_cost(add:"pay ",param1)} for each age counter on it. + reminder: At the beginning of your upkeep, put an age counter on this permanent, then sacrifice it unless you {for_mana_costs(add:"pay ",param1)} for each age counter on it. keyword: keyword: Horsemanship match: Horsemanship @@ -1264,19 +1259,19 @@ keyword: reminder: This creature can block or be blocked only by creatures with shadow. keyword: keyword: Buyback - match: Buybackcost + match: Buyback cost mode: expert - reminder: You may {format_cost(add:"pay ",param1)} in addition to any other costs as you play this spell. If you do, put this card into your hand as it resolves. + reminder: You may {for_mana_costs(add:"pay ",param1)} in addition to any other costs as you play this spell. If you do, put this card into your hand as it resolves. keyword: keyword: Echo - match: Echocost + match: Echo cost mode: expert reminder: At the beginning of your upkeep, if this came under your control since the beginning of your last upkeep, sacrifice it unless you pay its echo cost. keyword: keyword: Cardcycling - match: prefixcyclingcost + match: prefixcycling cost mode: expert - reminder: {format_cost(param2)}, Discard this card: Search your library for a {param1} card, reveal it, and put it into your hand. Then shuffle your library. + reminder: {param2}, Discard this card: Search your library for a {param1} card, reveal it, and put it into your hand. Then shuffle your library. keyword: keyword: Fading match: Fading number @@ -1284,22 +1279,22 @@ keyword: reminder: This comes into play with {english_number_a(param1)} fade counter(s) on it. At the beginning of your upkeep, remove a fade counter from it. If you can’t, sacrifice it. keyword: keyword: Kicker - match: Kickercost + match: Kicker cost mode: expert - reminder: You may {format_cost(add:"pay an additional ",param1)} as you play this spell. + reminder: You may {for_mana_costs(add:"pay an additional ",param1)} as you play this spell. keyword: keyword: Madness - match: Madnesscost + match: Madness cost mode: expert reminder: You may play this card for its madness cost at the time you discard it. keyword: keyword: Flashback - match: Flashbackcost + match: Flashback cost mode: expert reminder: You may play this card from your graveyard for its flashback cost. Then remove it from the game. keyword: keyword: Morph - match: Morphcost + match: Morph cost mode: expert reminder: You may play this face down as a 2/2 creature for 3. Turn it face up any time for its morph cost. keyword: @@ -1326,17 +1321,17 @@ keyword: keyword: Affinity for match: Affinity for name mode: expert - reminder: This spell costs 1 less to play for each {param1} you control. + reminder: This spell costs 1 less to play for each {english_singular(param1)} you control. keyword: keyword: Entwine - match: Entwinecost + match: Entwine cost mode: expert reminder: Choose both if you pay the entwine cost. keyword: keyword: Equip - match: Equipcost + match: Equip cost mode: expert - reminder: {format_cost(param1)}: Attach to target creature you control. Equip only as a sorcery. + reminder: {param1}: Attach to target creature you control. Equip only as a sorcery. keyword: keyword: Imprint match: Imprint—action @@ -1380,9 +1375,9 @@ keyword: reminder: When this blocks or becomes blocked, it gets +{param1}/+{param1} until end of turn. keyword: keyword: Ninjutsu - match: Ninjutsucost + match: Ninjutsu cost mode: expert - reminder: {format_cost(param1)}, Return an unblocked attacker you control to hand: Put this card into play from your hand tapped and attacking. + reminder: {param1}, Return an unblocked attacker you control to hand: Put this card into play from your hand tapped and attacking. keyword: keyword: Soulshift match: Soulshift number @@ -1400,9 +1395,9 @@ keyword: reminder: Each creature you tap while playing this spell reduces its cost by 1 or by one mana of that creature’s color. keyword: keyword: Transmute - match: Transmutecost + match: Transmute cost mode: expert - reminder: {format_cost(param1)}, Discard this card: Search your library for a card with the same converted mana cost as this card, reveal it, and put it into your hand. Then shuffle your library. Play only as a sorcery. + reminder: {param1}, Discard this card: Search your library for a card with the same converted mana cost as this card, reveal it, and put it into your hand. Then shuffle your library. Play only as a sorcery. keyword: keyword: Haunt match: Haunt @@ -1415,7 +1410,7 @@ keyword: reminder: If an opponent was dealt damage this turn, this creature comes into play with {english_number_a(param1)} +1/+1 counter(s) on it. keyword: keyword: Replicate - match: Replicatecost + match: Replicate cost mode: expert reminder: When you play this spell, copy it for each time you paid its replicate cost. You may choose new targets for the copies. keyword: @@ -1432,7 +1427,7 @@ keyword: keyword: Protection from match: Protection from name mode: core - reminder: This creature can’t be blocked, targeted, dealt damage, or enchanted, or equipped by anything {param1}. + reminder: This creature can’t be blocked, targeted, dealt damage, or enchanted, or equipped by anything {english_singular(param1)}. keyword: keyword: Dredge match: Dredge number @@ -1445,14 +1440,14 @@ keyword: reminder: This creature comes into play with {english_number_a(param1)} +1/+1 counter(s) on it. Whenever another creature comes into play, you may move a +1/+1 counter from this creature onto it. keyword: keyword: Forecast - match: Forecastcost, Reveal name from your hand: action + match: Forecast cost, Reveal name from your hand: action mode: expert reminder: Play this ability only during your upkeep and only once each turn. keyword: keyword: Recover - match: Recovercost + match: Recover cost mode: expert - reminder: When a creature is put into your graveyard from play, you may {format_cost(add:"pay ",param1)}. If you do, return this card from your graveyard to your hand. Otherwise, remove this card from the game. + reminder: When a creature is put into your graveyard from play, you may {for_mana_costs(add:"pay ",param1)}. If you do, return this card from your graveyard to your hand. Otherwise, remove this card from the game. keyword: keyword: Ripple match: Ripple number @@ -1510,14 +1505,14 @@ keyword: reminder: Look at the top {english_number_multiple(param1)} card(s) of an opponent's library, then put any number of them on the bottom of that player's library and the rest on top in any order. keyword: keyword: Transfigure - match: Transfigurecost + match: Transfigure cost mode: expert - reminder: {format_cost(param1)}, Sacrifice this creature: Search your library for a creature card with the same converted mana cost as this creature and put that card into play. Then shuffle your library. Play only as a sorcery. + reminder: {param1}, Sacrifice this creature: Search your library for a creature card with the same converted mana cost as this creature and put that card into play. Then shuffle your library. Play only as a sorcery. keyword: keyword: Aura swap - match: Aura swapcost + match: Aura swap cost mode: expert - reminder: {format_cost(param1)}: Exchange this Aura with an Aura card in your hand. + reminder: {param1}: Exchange this Aura with an Aura card in your hand. keyword: keyword: Frenzy match: Frenzy number @@ -1568,6 +1563,6 @@ keyword: mode: pseudo keyword: keyword: Fortify - match: Fortifycost + match: Fortify cost mode: expert - reminder: {format_cost(param1)}: Attach to target land you control. Fortify only as a sorcery. This card comes into play unattached and stays in play if the land leaves play. + reminder: {param1}: Attach to target land you control. Fortify only as a sorcery. This card comes into play unattached and stays in play if the land leaves play. diff --git a/src/data/keyword.cpp b/src/data/keyword.cpp index 4ea72a78..4b3ef5a3 100644 --- a/src/data/keyword.cpp +++ b/src/data/keyword.cpp @@ -16,11 +16,13 @@ DECLARE_TYPEOF_COLLECTION(KeywordP); DECLARE_TYPEOF_COLLECTION(KeywordModeP); DECLARE_TYPEOF_COLLECTION(KeywordParamP); DECLARE_TYPEOF_COLLECTION(const Keyword*); +DECLARE_POINTER_TYPE(KeywordParamValue); // ----------------------------------------------------------------------------- : Reflection KeywordParam::KeywordParam() : optional(true) + , eat_separator(true) {} IMPLEMENT_REFLECTION(ParamReferenceType) { @@ -35,7 +37,11 @@ IMPLEMENT_REFLECTION(KeywordParam) { REFLECT(placeholder); REFLECT(optional); REFLECT(match); + REFLECT(separator_before_is); + REFLECT(eat_separator); REFLECT(script); + REFLECT(reminder_script); + REFLECT(separator_script); REFLECT(example); REFLECT(refer_scripts); } @@ -77,6 +83,49 @@ IMPLEMENT_REFLECTION(Keyword) { REFLECT(mode); } +/*//%% +String KeywordParam::make_separator_before() const { + // decode regex; find a string that matches it + String ret; + int disabled = 0; + for (size_t i = 0 ; i < separator_before_is.size() ; ++i) { + Char c = separator_before_is.GetChar(i); + if (c == _('(')) { + if (disabled) ++disabled; + } else if (c == _(')')) { + if (disabled) --disabled; + } else if (!disabled) { + if (c == _('|')) { + disabled = 1; // disable after | + } else if (c == _('+') || c == _('*') || c == _('?') || c == _('^') || c == _('$')) { + // ignore + } else if (c == _('\\') && i + 1 < separator_before_is.size()) { + // escape + ret += separator_before_is.GetChar(++i); + } else if (c == _('[') && i + 1 < separator_before_is.size()) { + // character class + c = separator_before_is.GetChar(++i); + if (c != _('^')) ret += c; + // ignore the rest of the class + for ( ++i ; i < separator_before_is.size() ; ++i) { + c = separator_before_is.GetChar(i); + if (c == _(']')) break; + } + } else { + ret += c; + } + } + } + return ret; +}*/ +void KeywordParam::compile() { + if (!separator_before_is.empty() && !separator_before_re.IsValid()) { + separator_before_re.Compile(_("^") + separator_before_is, wxRE_ADVANCED); + if (eat_separator) { + separator_before_eat.Compile(separator_before_is + _("$"), wxRE_ADVANCED); + } + } +} size_t Keyword::findMode(const vector& modes) const { // find @@ -139,12 +188,19 @@ String regex_escape(Char c) { return String(1,c); } } +/// Escape a string for use in regular expressions +String regex_escape(const String& s) { + String ret; + FOR_EACH_CONST(c,s) ret += regex_escape(c); + return ret; +} void Keyword::prepare(const vector& param_types, bool force) { - if (!force && matchRe.IsValid()) return; + if (!force && match_re.IsValid()) return; parameters.clear(); // Prepare regex - String regex = _("("); + String regex; + String text; // normal, non-regex, text vector::const_iterator param = parameters.begin(); // Parse the 'match' string for (size_t i = 0 ; i < match.size() ;) { @@ -162,21 +218,37 @@ void Keyword::prepare(const vector& param_types, bool force) { } } if (!p) { - throw InternalError(_("Unknown keyword parameter type: ") + type); + // throwing an error can mean a set will not be loaded! + // instead, simply disable the keyword + //throw InternalError(_("Unknown keyword parameter type: ") + type); + handle_error(_("Unknown keyword parameter type: ") + type, true, false); + valid = false; + return; } parameters.push_back(p); - // modify regex - regex += _(")(") + make_non_capturing(p->match) + _(")") + (p->optional ? _("?") : _("")) + _("("); + // modify regex : match text before + p->compile(); + if (p->separator_before_eat.IsValid() && p->separator_before_eat.Matches(text)) { + // remove the separator from the text to prevent duplicates + size_t start, len; + p->separator_before_eat.GetMatch(&start, &len); + text = text.substr(0, start); + } + regex += _("(") + regex_escape(text) + _(")"); + text.clear(); + // modify regex : match parameter + regex += _("(") + make_non_capturing(p->match) + (p->optional ? _(")?") : _(")")); i = skip_tag(match, end); } else { - regex += regex_escape(c); + text += c; i++; } } - regex += _(")"); - if (matchRe.Compile(regex, wxRE_ADVANCED)) { + regex += _("(") + regex_escape(text) + _(")"); + regex = _("\\y") + regex + _("(?=$|[^a-zA-Z0-9])"); // only match whole words + if (match_re.Compile(regex, wxRE_ADVANCED)) { // not valid if it matches "", that would make MSE hang - valid = !matchRe.Matches(_("")); + valid = !match_re.Matches(_("")); } else { valid = false; throw InternalError(_("Error creating match regex")); @@ -263,22 +335,40 @@ void KeywordDatabase::add(const vector& kws) { void KeywordDatabase::add(const Keyword& kw) { if (kw.match.empty() || !kw.valid) return; // can't handle empty keywords + // Create root if (!root) { root = new KeywordTrie; root->on_any_star = root; } KeywordTrie* cur = root->insertAnyStar(); + // Add to trie + String text; // normal text + size_t param = 0; for (size_t i = 0 ; i < kw.match.size() ;) { Char c = kw.match.GetChar(i); if (is_substr(kw.match, i, _("separator_before_eat; + if (sep.IsValid() && sep.Matches(text)) { + // remove the separator from the text to prevent duplicates + size_t start, len; + sep.GetMatch(&start, &len); + text = text.substr(0, start); + } + } + ++param; + // match anything + cur = cur->insert(text); + text.clear(); cur = cur->insertAnyStar(); i = match_close_tag_end(kw.match, i); } else { - cur = cur->insert(c); + text += c; i++; } } + cur = cur->insert(text); // now cur is the trie after matching the keyword anywhere in the input text cur->finished.push_back(&kw); } @@ -383,26 +473,27 @@ String KeywordDatabase::expand(const String& text, const Keyword* kw = f; if (used.insert(kw).second) { // we have found a possible match, which we have not seen before - assert(kw->matchRe.IsValid()); + assert(kw->match_re.IsValid()); // try to match it against the *untagged* string - if (kw->matchRe.Matches(untagged)) { + if (kw->match_re.Matches(untagged)) { // Everything before the keyword size_t start_u, len_u; - kw->matchRe.GetMatch(&start_u, &len_u, 0); + kw->match_re.GetMatch(&start_u, &len_u, 0); size_t start = untagged_to_index(s, start_u, true), end = untagged_to_index(s, start_u + len_u, true); result += s.substr(0, start); // Split the keyword, set parameters in context String total; // the total keyword - size_t match_count = kw->matchRe.GetMatchCount(); + size_t match_count = kw->match_re.GetMatchCount(); assert(match_count - 1 == 1 + 2 * kw->parameters.size()); for (size_t j = 1 ; j < match_count ; ++j) { - // j = odd -> text - // j = even -> parameter #(j/2) + // we start counting at 1, so + // j = 1 mod 2 -> text + // j = 0 mod 2 -> parameter #((j-1)/2) == (j/2-1) size_t start_u, len_u; - kw->matchRe.GetMatch(&start_u, &len_u, j); + kw->match_re.GetMatch(&start_u, &len_u, j); // note: start_u can be (uint)-1 when len_u == 0 size_t part_end = len_u > 0 ? untagged_to_index(s, start_u + len_u, true) : start; String part = s.substr(start, part_end - start); @@ -410,24 +501,45 @@ String KeywordDatabase::expand(const String& text, // parameter KeywordParam& kwp = *kw->parameters[j/2-1]; String param = untagged.substr(start_u, len_u); // untagged version + // strip separator + String separator; + if (kwp.separator_before_re.IsValid()) { + if (kwp.separator_before_re.Matches(param)) { + size_t s_start, s_len; // start should be 0 + kwp.separator_before_re.GetMatch(&s_start, &s_len); + separator = param.substr(0, s_start + s_len); + param = param.substr(s_start + s_len); + // strip from tagged version + size_t end_t = untagged_to_index(part, s_start + s_len, false); + part = get_tags(part, 0, end_t, true, true) + part.substr(end_t); + // transform? + if (kwp.separator_script) { + ctx.setVariable(_("input"), to_script(separator)); + separator = kwp.separator_script.invoke(ctx)->toString(); + } + } + } + // to script + KeywordParamValueP script_param(new KeywordParamValue(kwp.name, separator, param)); + KeywordParamValueP script_part (new KeywordParamValue(kwp.name, separator, part)); + // process param if (param.empty()) { // placeholder - param = _("") + (kwp.placeholder.empty() ? kwp.name : kwp.placeholder) + _(""); - part = part + param; // keep tags - } else if (kw->parameters[j/2-1]->script) { + script_param->value = _("") + (kwp.placeholder.empty() ? kwp.name : kwp.placeholder) + _(""); + script_part->value = part + script_param->value; // keep tags + } else { // apply parameter script - ctx.setVariable(_("input"), to_script(part)); - part = kwp.script.invoke(ctx)->toString(); - ctx.setVariable(_("input"), to_script(part)); - param = kwp.script.invoke(ctx)->toString(); + if (kwp.script) { + ctx.setVariable(_("input"), script_part); + script_part->value = kwp.script.invoke(ctx)->toString(); + } + if (kwp.reminder_script) { + ctx.setVariable(_("input"), script_param); + script_param->value = kwp.reminder_script.invoke(ctx)->toString(); + } } - String param_type = replace_all(replace_all(replace_all(kwp.name, - _("("),_("-")), - _(")"),_("-")), - _(" "),_("-")); - part = _("") + part + _(""); - param = _("") + param + _(""); - ctx.setVariable(String(_("param")) << (int)(j/2), to_script(param)); + part = separator + script_part->toString(); + ctx.setVariable(String(_("param")) << (int)(j/2), script_param); } total += part; start = part_end; @@ -476,3 +588,22 @@ String KeywordDatabase::expand(const String& text, return result; } + +// ----------------------------------------------------------------------------- : KeywordParamValue + +ScriptType KeywordParamValue::type() const { return SCRIPT_STRING; } +String KeywordParamValue::typeName() const { return _("keyword parameter"); } +KeywordParamValue::operator String() const { + String safe_type = replace_all(replace_all(replace_all(type_name, + _("("),_("-")), + _(")"),_("-")), + _(" "),_("-")); + return _("") + value + _(""); +} +ScriptValueP KeywordParamValue::getMember(const String& name) const { + if (name == _("type")) return to_script(type_name); + if (name == _("separator")) return to_script(separator); + if (name == _("value")) return to_script(value); + if (name == _("param")) return to_script(value); + return ScriptValue::getMember(name); +} diff --git a/src/data/keyword.hpp b/src/data/keyword.hpp index 87df8674..cf0d018e 100644 --- a/src/data/keyword.hpp +++ b/src/data/keyword.hpp @@ -34,15 +34,28 @@ class ParamReferenceType : public IntrusivePtrBase { class KeywordParam : public IntrusivePtrBase { public: KeywordParam(); - String name; ///< Name of the parameter type - String description; ///< Description of the parameter type - String placeholder; ///< Placholder for , name is used if this is empty - bool optional; ///< Can this parameter be left out (a placeholder is then used) - String match; ///< Regular expression to match - OptionalScript script; ///< Transformation of the value for showing in the reminder text - String example; ///< Example for the keyword editor + String name; ///< Name of the parameter type + String description; ///< Description of the parameter type + String placeholder; ///< Placholder for , name is used if this is empty + bool optional; ///< Can this parameter be left out (a placeholder is then used) + String match; ///< Regular expression to match (including separators) + String separator_before_is; ///< Regular expression of separator before the param + wxRegEx separator_before_re; ///< Regular expression of separator before the param, compiled + wxRegEx separator_before_eat;///< Regular expression of separator before the param, if eat_separator + bool eat_separator; ///< Remove the separator from the match string if it also appears there (prevent duplicates) + OptionalScript script; ///< Transformation of the value for showing as the parameter + OptionalScript reminder_script; ///< Transformation of the value for showing in the reminder text + OptionalScript separator_script; ///< Transformation of the separator + String example; ///< Example for the keyword editor vector refer_scripts;///< Way to refer to a parameter from the reminder text script +//% /// Make a string that can function as a separator before the parameter +//% /** This tries to decode the separator_before_is regex */ +//% String make_separator_before() const; + + /// Compile regexes + void compile(); + DECLARE_REFLECTION(); }; @@ -76,9 +89,9 @@ class Keyword : public IntrusivePtrVirtualBase { /// Regular expression to match and split parameters, automatically generated. /** The regex has exactly 2 * parameters.size() + 1 captures (excluding the entire match, caputure 0), * captures 1,3,... capture the plain text of the match string - * captures 2,4,... capture the parameters + * captures 2,4,... capture the separators and parameters */ - wxRegEx matchRe; + wxRegEx match_re; bool fixed; ///< Is this keyword uneditable? (true for game keywods, false for set keywords) bool valid; ///< Is this keyword okay (reminder text compiles & runs; match does not match "") @@ -132,5 +145,23 @@ class KeywordDatabase { KeywordTrie* root; ///< Data structure for finding keywords }; +// ----------------------------------------------------------------------------- : Processing parameters + +/// A script value containing the value of a keyword parameter +class KeywordParamValue : public ScriptValue { + public: + KeywordParamValue(const String& type, const String& separator, const String& value) + : type_name(type), separator(separator), value(value) + {} + String type_name; + String separator; + String value; + + virtual ScriptType type() const; + virtual String typeName() const; + virtual operator String() const; + virtual ScriptValueP getMember(const String& name) const; +}; + // ----------------------------------------------------------------------------- : EOF #endif diff --git a/src/script/functions/english.cpp b/src/script/functions/english.cpp index 8a228b49..f4ca2a74 100644 --- a/src/script/functions/english.cpp +++ b/src/script/functions/english.cpp @@ -70,7 +70,7 @@ String do_english_num(String input, String(*fun)(int)) { // a keyword parameter, of the form "123" size_t start = skip_tag(input, 0); if (start != String::npos) { - size_t end = input.find_first_of(_('<'), start); + size_t end = input.find_first_of(_('<'), start); if (end != String::npos) { String is = input.substr(start, end - start); long i = 0; @@ -105,6 +105,54 @@ SCRIPT_FUNCTION(english_number_multiple) { SCRIPT_RETURN(do_english_num(input, english_number_multiple)); } +// ----------------------------------------------------------------------------- : Singular/plural + +String english_singular(const String& str) { + if (str.size() > 3 && is_substr(str, str.size()-3, _("ies"))) { + return str.substr(0, str.size() - 3) + _("y"); + } else if (str.size() > 1 && str.GetChar(str.size() - 1) == _('s')) { + return str.substr(0, str.size() - 1); + } else { + return str; + } +} +String english_plural(const String& str) { + if (str.size() > 1 && str.GetChar(str.size() - 1) == _('y')) { + return str.substr(0, str.size() - 1) + _("ies"); + } else if (str.size() > 1 && str.GetChar(str.size() - 1) == _('s')) { + return str + _("es"); + } else { + return str + _("s"); + } +} + +// script_english_singular/plural/singplur +String do_english(String input, String(*fun)(const String&)) { + if (is_substr(input, 0, _("123" + size_t start = skip_tag(input, 0); + if (start != String::npos) { + size_t end = input.find_first_of(_('<'), start); + if (end != String::npos) { + String is = input.substr(start, end - start); + return substr_replace(input, start, end, fun(is)); + } + } + return input; // failed + } else { + return fun(input); + } +} + +SCRIPT_FUNCTION(english_singular) { + SCRIPT_PARAM(String, input); + SCRIPT_RETURN(do_english(input, english_singular)); +} +SCRIPT_FUNCTION(english_plural) { + SCRIPT_PARAM(String, input); + SCRIPT_RETURN(do_english(input, english_plural)); +} + // ----------------------------------------------------------------------------- : Hints bool is_vowel(Char c) { @@ -160,6 +208,24 @@ String process_english_hints(const String& str) { } ret += c; ++i; + } else if (is_substr(str, i, _(""))) { + // singular -> keep, plural -> drop + size_t start = skip_tag(str, i); + size_t end = match_close_tag(str, start); + if (singplur == 1 && end != String::npos) { + ret += str.substr(start, end - start); + } + singplur = 0; + i = skip_tag(str, end); + } else if (is_substr(str, i, _(""))) { + // singular -> drop, plural -> keep + size_t start = skip_tag(str, i); + size_t end = match_close_tag(str, start); + if (singplur == 2 && end != String::npos) { + ret += str.substr(start, end - start); + } + singplur = 0; + i = skip_tag(str, end); } else if (c == _('(') && singplur) { // singular -> drop (...), plural -> keep it size_t end = str.find_first_of(_(')'), i); @@ -192,5 +258,7 @@ void init_script_english_functions(Context& ctx) { ctx.setVariable(_("english number"), script_english_number); ctx.setVariable(_("english number a"), script_english_number_a); ctx.setVariable(_("english number multiple"), script_english_number_multiple); + ctx.setVariable(_("english singular"), script_english_singular); + ctx.setVariable(_("english plural"), script_english_plural); ctx.setVariable(_("process english hints"), script_process_english_hints); } diff --git a/src/util/io/reader.cpp b/src/util/io/reader.cpp index f176ee87..bccf2bdf 100644 --- a/src/util/io/reader.cpp +++ b/src/util/io/reader.cpp @@ -132,6 +132,7 @@ void Reader::readLine(bool in_string) { } key = cannocial_name_form(trim(key)); value = pos == String::npos ? _("") : trim_left(line.substr(pos+1)); + if (key.empty() && pos!=String::npos) key = _(" "); // we don't want an empty key if there was a colon } void Reader::unknownKey() { @@ -162,7 +163,7 @@ void Reader::unknownKey() { return; } } - if (indent == expected_indent) { + if (indent >= expected_indent) { warning(_("Unexpected key: '") + key + _("'")); do { moveNext(); diff --git a/src/util/tagged_string.cpp b/src/util/tagged_string.cpp index 37a7f1b1..258bc661 100644 --- a/src/util/tagged_string.cpp +++ b/src/util/tagged_string.cpp @@ -386,11 +386,6 @@ String remove_tag_contents(const String& str, const String& tag) { // ----------------------------------------------------------------------------- : Updates -/// Return all open or close tags in the given range from a string -/** for example: - * if close_tags == false, "texttexttext" --> "" - * if close_tags == true, "texttexttext" --> "" - */ String get_tags(const String& str, size_t start, size_t end, bool open_tags, bool close_tags) { String ret; bool intag = false; diff --git a/src/util/tagged_string.hpp b/src/util/tagged_string.hpp index fb913261..b6807e15 100644 --- a/src/util/tagged_string.hpp +++ b/src/util/tagged_string.hpp @@ -143,6 +143,13 @@ String remove_tag_contents(const String& str, const String& tag); // ----------------------------------------------------------------------------- : Updates +/// Return all open or close tags in the given range from a string +/** for example: + * if close_tags == false, "texttexttext" --> "" + * if close_tags == true, "texttexttext" --> "" + */ +String get_tags(const String& str, size_t start, size_t end, bool open_tags, bool close_tags); + /// Replace a subsection of 'input' with 'replacement'. /** The section to replace is indicated by [start...end). * This function makes sure tags still match. It also attempts to cancel out tags.