diff --git a/data/en.mse-locale/locale b/data/en.mse-locale/locale
index 8ac80d15..b9d25752 100644
--- a/data/en.mse-locale/locale
+++ b/data/en.mse-locale/locale
@@ -260,8 +260,11 @@ error:
unable to store file: Error while saving, unable to store file
# Script stuff
- collection has no member: Collection has no member '%s'
- object has no member: Object has no member '%s'
+ has no member: %s has no member '%s'
+ can't convert: Can't convert from %s to %s
+ has no member value: String "%s" has no member '%s'
+ can't convert value: Can't convert "%s" from %s to %s
+ unsupported format: Invalid string format: '%s'
# Image stuff
coordinates for blending overlap: Coordinates for blending overlap
@@ -287,9 +290,15 @@ error:
############################################################## Types used in scripts
type:
- real: Real number
- integer: Integer number
- string: String
+ function: function
+ collection: collection
+ object: object
+ real: real number
+ integer: integer number
+ string: string
+ boolean: boolean
+ color: color
+ nil: nothing
############################################################## Magic
game:
diff --git a/data/magic.mse-game/game b/data/magic.mse-game/game
index 5cce5684..66dfed6a 100644
--- a/data/magic.mse-game/game
+++ b/data/magic.mse-game/game
@@ -161,18 +161,22 @@ init script:
# after: ")",
# fix_english: true
# ) +
- # step 3 : expand shortcut words ~ and CARDNAME
+ expand_keywords_rule(
+ default_expand: { set.automatic_reminder_text == "yes" },
+ combine: { "{keyword} ({reminder})" }
+ ) +
+ # step 3a : expand shortcut words ~ and CARDNAME
replace_rule(
match: "~|~THIS~|CARDNAME",
in_context: "(^|[[:space:]]|\\()", # TODO: Allow any punctuation before
replace: ""
) +
- # step 4 : fill in atom fields
+ # step 3b : fill in atom fields
tag_contents_rule(
tag: "",
contents: { if card_name=="" then "CARDNAME" else card_name }
) +
- # step 4.5 : explict non mana symbols
+ # step 4 : explict non mana symbols
replace_rule(
match: "\\][STXYZWUBRG0-9/|]+\\[",
replace: {"" + mana_filter_t() + ""} ) +
@@ -802,7 +806,7 @@ keyword parameter type:
keyword parameter type:
name: cost
#insert as: word
- match: ***:[XYZ0-9WUBRGS/]+|[^(.,\n]|([XYZ0-9WUBRGS/]+,)?[^(.,\n]*
+ match: [XYZ0-9WUBRGS/]+|[^(.,\n]|([XYZ0-9WUBRGS/]+,)?[^(.,\n]*
keyword parameter type:
name: number
match: [XYZ0-9]+
@@ -821,11 +825,11 @@ keyword parameter type:
keyword parameter type:
name: action
#insert as: word
- match: ***:[^(.,\n]+
+ match: [^(.,\n]+
keyword parameter type:
name: name
#insert as: word
- match: ***:[^(.,\n]+
+ match: [^(.,\n]+
############################# All Magic keywords
# By JrEye and Neko_Asakami
@@ -851,7 +855,7 @@ keyword:
keyword: Cycling
separator: whitespace [ ]
parameter: cost
- reminder: , Discard this card: Draw a card.
+ reminder: {param1}, Discard this card: Draw a card.
keyword:
keyword: Trample
reminder: If this creature would deal enough combat damage to its blockers to destroy them, you may have it deal the rest of its damage to defending player.
diff --git a/src/data/action/symbol.cpp b/src/data/action/symbol.cpp
index 0b8c6197..53a053d9 100644
--- a/src/data/action/symbol.cpp
+++ b/src/data/action/symbol.cpp
@@ -9,8 +9,8 @@
#include
#include
-DECLARE_TYPEOF_COLLECTION((pair));
-DECLARE_TYPEOF_COLLECTION((pair));
+DECLARE_TYPEOF_COLLECTION(pair);
+DECLARE_TYPEOF_COLLECTION(pair);
DECLARE_TYPEOF_COLLECTION(SymbolPartP);
DECLARE_TYPEOF_COLLECTION(ControlPointP);
@@ -204,11 +204,11 @@ void SymbolPartScaleAction::update() {
// the size after the move
newMin = newRealMin; newSize = newRealSize;
if (constrain && scaleX != 0 && scaleY != 0) {
- // TODO : snapping
Vector2D scale = newSize.div(tmpSize);
scale = constrain_vector(scale, true, true);
newSize = tmpSize.mul(scale);
newMin += (newRealSize - newSize).mul(Vector2D(scaleX == -1 ? 1 : 0, scaleY == -1 ? 1 : 0));
+ // TODO : snapping
} else if (snap >= 0) {
if (scaleX + scaleY < 0) {
newMin = snap_vector(newMin, snap);
diff --git a/src/data/card.cpp b/src/data/card.cpp
index cf697086..6c01b41b 100644
--- a/src/data/card.cpp
+++ b/src/data/card.cpp
@@ -13,7 +13,7 @@
#include
DECLARE_TYPEOF_COLLECTION(FieldP);
-DECLARE_TYPEOF_NO_REV((IndexMap));
+DECLARE_TYPEOF_NO_REV(IndexMap);
// ----------------------------------------------------------------------------- : Card
diff --git a/src/data/field/choice.cpp b/src/data/field/choice.cpp
index 9b11c2fa..eacedc77 100644
--- a/src/data/field/choice.cpp
+++ b/src/data/field/choice.cpp
@@ -10,7 +10,7 @@
#include
DECLARE_TYPEOF_COLLECTION(ChoiceField::ChoiceP);
-DECLARE_TYPEOF((map));
+DECLARE_TYPEOF(map);
// ----------------------------------------------------------------------------- : ChoiceField
diff --git a/src/data/format/image_to_symbol.cpp b/src/data/format/image_to_symbol.cpp
index 340f5d14..d457eb36 100644
--- a/src/data/format/image_to_symbol.cpp
+++ b/src/data/format/image_to_symbol.cpp
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
DECLARE_TYPEOF_COLLECTION(ControlPointP);
DECLARE_TYPEOF_COLLECTION(SymbolPartP);
@@ -99,7 +100,7 @@ bool is_mse1_symbol(const Image& img) {
int r = *d++;
int g = *d++;
int b = *d++;
- delta += fabs(r - b) + fabs(r - g) + fabs(b - g);
+ delta += abs(r - b) + abs(r - g) + abs(b - g);
}
}
if (delta > 5000) return false; // not black & white enough
@@ -357,7 +358,7 @@ void straighten(SymbolPart& part) {
Vector2D bb = next.delta_before.normalized();
// if the area beneath the polygon formed by the handles is small
// then it is a straight line
- double cpDot = fabs(aa.cross(ab)) + fabs(bb.cross(ab));
+ double cpDot = abs(aa.cross(ab)) + abs(bb.cross(ab));
if (cpDot < treshold) {
cur.segment_after = next.segment_before = SEGMENT_LINE;
cur.delta_after = next.delta_before = Vector2D();
@@ -374,7 +375,7 @@ void merge_lines(SymbolPart& part) {
Vector2D a = part.getPoint(i-1)->pos, b = cur.pos, c = part.getPoint(i+1)->pos;
Vector2D ab = (a-b).normalized();
Vector2D bc = (b-c).normalized();
- double angle_len = fabs( atan2(ab.x,ab.y) - atan2(bc.x,bc.y)) * (a-c).lengthSqr();
+ double angle_len = abs( atan2(ab.x,ab.y) - atan2(bc.x,bc.y)) * (a-c).lengthSqr();
bool keep = angle_len >= .0001;
if (!keep) {
part.points.erase(part.points.begin() + i);
diff --git a/src/data/keyword.cpp b/src/data/keyword.cpp
index ef636097..5c0f5ea7 100644
--- a/src/data/keyword.cpp
+++ b/src/data/keyword.cpp
@@ -10,7 +10,12 @@
#include
class KeywordTrie;
-DECLARE_TYPEOF((map));
+DECLARE_TYPEOF(map);
+DECLARE_TYPEOF_COLLECTION(KeywordTrie*);
+DECLARE_TYPEOF_COLLECTION(KeywordP);
+DECLARE_TYPEOF_COLLECTION(KeywordParamP);
+DECLARE_TYPEOF_COLLECTION(KeywordExpansionP);
+DECLARE_TYPEOF_COLLECTION(const KeywordExpansion*);
// ----------------------------------------------------------------------------- : Reflection
@@ -58,10 +63,91 @@ IMPLEMENT_REFLECTION(Keyword) {
read_compat(tag, this);
REFLECT(expansions);
REFLECT(rules);
- REFLECT(mode);
+// REFLECT(mode);
}
+// ----------------------------------------------------------------------------- : Regex stuff
+
+/// Make sure the given regex does no capturing
+/** Basicly replaces "(" with "(?:" */
+String make_non_capturing(const String& re) {
+ String ret;
+ bool escape = false, bracket = false, capture = false;
+ FOR_EACH_CONST(c, re) {
+ if (capture && c != _('?')) {
+ // change this capture into a non-capturing "(" by appending "?:"
+ ret += _("?:");
+ capture = false;
+ }
+ if (escape) { // second char of escape sequence
+ escape = false;
+ } else if (c == _('\\')) { // start of escape sequence
+ escape = true;
+ } else if (c == _('[')) { // start of [...]
+ bracket = true;
+ } else if (c == _(']')) { // end of [...]
+ bracket = false;
+ } else if (bracket && c == _('(')) {
+ // wx has a bug, it counts the '(' in "[(]" as a matching group
+ // escape it so wx doesn't see it
+ ret += _('\\');
+ } else if (c == _('(')) { // start of capture?
+ capture = true;
+ }
+ ret += c;
+ }
+ return ret;
+}
+
+/// Escape a single character for use in regular expressions
+String regex_escape(Char c) {
+ if (c == _('(') || c == _(')') || c == _('[') || c == _(']') || c == _('{') ||
+ c == _('.') || c == _('^') || c == _('$') || c == _('#') || c == _('\\') ||
+ c == _('|') || c == _('+') || c == _('*') || c == _('?')) {
+ // c needs to be escaped
+ return _("\\") + String(1,c);
+ } else {
+ return String(1,c);
+ }
+}
+
+void KeywordExpansion::prepare(const vector& param_types, bool force) {
+ if (!force && matchRe.IsValid()) return;
+ parameters.clear();
+ // Prepare regex
+// String regex;
+ vector::const_iterator param = parameters.begin();
+ // Parse the 'match' string
+ for (size_t i = 0 ; i < match.size() ;) {
+ Char c = match.GetChar(i);
+ if (is_substr(match, i, _("name == type) {
+ p = pt.get();
+ break;
+ }
+ }
+ if (!p) {
+ throw InternalError(_("Unknown keyword parameter type: ") + type);
+ }
+ // modify regex
+ regex += _("(") + make_non_capturing(p->match) + _(")");
+ i = skip_tag(match, end);
+ } else {
+ regex += regex_escape(c);
+ i++;
+ }
+ }
+ if (!matchRe.Compile(regex, wxRE_ADVANCED)) {
+ throw InternalError(_("Error creating match regex"));
+ }
+}
// ----------------------------------------------------------------------------- : KeywordTrie
@@ -71,10 +157,13 @@ class KeywordTrie {
KeywordTrie();
~KeywordTrie();
- map children; ///< children after a given character (owned)
- KeywordTrie* on_any_star; ///< children on /.*/ (owned)
- Keyword* finished; ///< keywords that end in this node
+ map children; ///< children after a given character (owned)
+ KeywordTrie* on_any_star; ///< children on /.*/ (owned or this)
+ vector finished; ///< keywords expansions that end in this node
+ /// Insert nodes representing the given character
+ /** return the node where the evaluation will be after matching the character */
+ KeywordTrie* insert(Char match);
/// Insert nodes representing the given string
/** return the node where the evaluation will be after matching the string */
KeywordTrie* insert(const String& match);
@@ -94,73 +183,245 @@ KeywordTrie::~KeywordTrie() {
FOR_EACH(c, children) {
delete c.second;
}
- delete on_any_star;
+ if (on_any_star != this) delete on_any_star;
}
+KeywordTrie* KeywordTrie::insert(Char c) {
+ KeywordTrie*& child = children[c];
+ if (!child) child = new KeywordTrie;
+ return child;
+}
KeywordTrie* KeywordTrie::insert(const String& match) {
KeywordTrie* cur = this;
FOR_EACH_CONST(c, match) {
- KeywordTrie*& child = cur->children[c];
- if (!child) child = new KeywordTrie;
- cur = child;
+ cur = cur->insert(c);
}
return cur;
}
KeywordTrie* KeywordTrie::insertAnyStar() {
if (!on_any_star) on_any_star = new KeywordTrie;
+ on_any_star->on_any_star = on_any_star; // circular reference to itself
return on_any_star;
}
-// ----------------------------------------------------------------------------- : KeywordMatcher
-
-/// State of the matching algorithm
-class KeywordMatcher {
- public:
- KeywordMatcher(const String& s);
- private:
- String str;
- size_t pos;
-};
// ----------------------------------------------------------------------------- : KeywordDatabase
-/// A database of keywords to allow for fast matching
-/** NOTE: keywords may not be altered after they are added to the database,
- * The database should be rebuild.
- */
-class KeywordDatabase {
- public:
- /// Add a keyword to be matched
- void addKeyword(const Keyword&);
-
- /// Find the first matching keyword, return its position
- size_t firstMatch(const String& input, Keyword* keyword);
-
- private:
- KeywordTrie root;
-};
+KeywordDatabase::KeywordDatabase()
+ : root(nullptr)
+{}
-void KeywordDatabase::addKeyword(const Keyword& kw) {
- // TODO
+KeywordDatabase::~KeywordDatabase() {
+ clear();
}
-// ----------------------------------------------------------------------------- : Using keywords
-
-KeywordDatabaseP new_keyword_database() {
- return new_shared();
-}
-void add_keyword(KeywordDatabase& db, const Keyword& kw) {
- db.addKeyword(kw);
+void KeywordDatabase::clear() {
+ delete root;
+ root = nullptr;
}
+void KeywordDatabase::add(const vector& kws) {
+ FOR_EACH_CONST(kw, kws) {
+ add(*kw);
+ }
+}
+void KeywordDatabase::add(const Keyword& kw) {
+ FOR_EACH_CONST(e, kw.expansions) {
+ add(*e);
+ }
+}
-String expand_keywords(const KeywordDatabase& db, const String& text) {
- // 1. Remove all old reminder texts
- String s = remove_tag_contents(text, _(""));
- // 2. Process keywords
+void KeywordDatabase::add(const KeywordExpansion& e) {
+ if (!root) {
+ root = new KeywordTrie;
+ root->on_any_star = root;
+ }
+ KeywordTrie* cur = root->insertAnyStar();
+ for (size_t i = 0 ; i < e.match.size() ;) {
+ Char c = e.match.GetChar(i);
+ if (is_substr(e.match, i, _("insertAnyStar();
+ i = match_close_tag_end(e.match, i);
+ } else {
+ cur = cur->insert(c);
+ i++;
+ }
+ }
+ // now cur is the trie after matching the keyword anywhere in the input text
+ cur->finished.push_back(&e);
+}
+
+void KeywordDatabase::prepare_parameters(const vector& ps, const vector& kws) {
+ FOR_EACH_CONST(kw, kws) {
+ prepare_parameters(ps, *kw);
+ }
+}
+void KeywordDatabase::prepare_parameters(const vector& ps, const Keyword& kw) {
+ FOR_EACH_CONST(e, kw.expansions) {
+ e->prepare(ps);
+ }
+}
+
+// ----------------------------------------------------------------------------- : KeywordDatabase : matching
+
+// transitive closure of a state, follow all on_any_star links
+void closure(vector& state) {
+ for (size_t j = 0 ; j < state.size() ; ++j) {
+ if (state[j]->on_any_star && state[j]->on_any_star != state[j]) {
+ state.push_back(state[j]->on_any_star);
+ }
+ }
+}
+
+//DEBUG
+void dump(int i, KeywordTrie* t) {
+ FOR_EACH(c, t->children) {
+ wxLogDebug(String(i,_(' ')) + c.first + _(" ") + String::Format(_("%p"),c.second));
+ dump(i+2, c.second);
+ }
+ if (t->on_any_star) {
+ wxLogDebug(String(i,_(' ')) + _(".*") + _(" ") + String::Format(_("%p"),t->on_any_star));
+ if (t->on_any_star != t) dump(i+2, t->on_any_star);
+ }
+}
+
+String KeywordDatabase::expand(const String& text,
+ const ScriptValueP& expand_default,
+ const ScriptValueP& combine_script,
+ Context& ctx) const {
+ // DEBUG: dump db
+ //dump(0, root);
- // TODO
+ assert(combine_script);
- return s;
+ // Remove all old reminder texts
+ String s = remove_tag_contents(text, _(""));
+ s = remove_tag_contents(s, _("")); // OLD, TODO: REMOVEME
+ s = remove_tag(s, _(" current; // current location(s) in the trie
+ vector next; // location(s) after this step
+ set used; // keywords already investigated
+ current.push_back(root);
+ closure(current);
+ char expand_type = 'a'; // is the keyword expanded? From tag
+ // Possible values are:
+ // - '0' = reminder text explicitly hidden
+ // - '1' = reminder text explicitly shown
+ // - 'a' = reminder text in default state, hidden
+ // - 'A' = reminder text in default state, shown
+
+ for (size_t i = 0 ; i < s.size() ;) {
+ Char c = s.GetChar(i);
+ // tag?
+ if (c == _('<')) {
+ if (is_substr(s, i, _("
+ s = s.erase(i, skip_tag(s,i)-i); // remove the tag from the string
+ } else if (is_substr(s, i, _("on_any_star) {
+ next.push_back(kt->on_any_star);
+ }
+ map::const_iterator it = kt->children.find(c);
+ if (it != kt->children.end()) {
+ next.push_back(it->second);
+ wxLogDebug(c + String(_(" -> ")) + String::Format(_("%p"), it->second));
+ }
+ }
+ // next becomes current
+ swap(current, next);
+ next.clear();
+ closure(current);
+ // are we done?
+ FOR_EACH(n, current) {
+ FOR_EACH(f, n->finished) {
+ const KeywordExpansion* kw = f;
+ if (used.insert(kw).second) {
+ // we have found a possible match, which we have not seen before
+ assert(kw->matchRe.IsValid());
+
+ // try to match it against the *untagged* string
+ if (kw->matchRe.Matches(untagged)) {
+ // Everything before the keyword
+ size_t start_u, len_u;
+ kw->matchRe.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);
+
+ // Set parameters in context
+ size_t match_count = kw->matchRe.GetMatchCount();
+ for (size_t j = 1 ; j < match_count ; ++j) {
+ size_t start_u, len_u;
+ kw->matchRe.GetMatch(&start_u, &len_u, j);
+ String param = untagged.substr(start_u, len_u);
+ if (j-1 < kw->parameters.size() && kw->parameters[j-1]->script) {
+ // apply parameter script
+ param = kw->parameters[j-1]->script.invoke(ctx)->toString();
+ }
+ ctx.setVariable(String(_("param")) << (int)j, toScript(param));
+ }
+ ctx.setVariable(_("mode"), toScript(kw->mode));
+
+ // Show reminder text?
+ bool expand = expand_type == _('1');
+ if (!expand && expand_type != _('0')) {
+ // default expand, determined by script
+ expand = expand_default && (bool)*expand_default->eval(ctx);
+ expand_type = expand ? _('A') : _('a');
+ }
+
+ // Combine keyword & reminder with result
+ if (expand) {
+ String reminder = kw->reminder.invoke(ctx)->toString();
+ ctx.setVariable(_("keyword"), toScript(s.substr(start, end - start)));
+ ctx.setVariable(_("reminder"), toScript(reminder));
+ result += _("");
+ result += combine_script->eval(ctx)->toString();
+ result += _("");
+ } else {
+ result += _("");
+ result += s.substr(start, end - start);
+ result += _("");
+ }
+
+ // After keyword
+ s = s.substr(end);
+ untagged = untagged.substr(start_u + len_u);
+ used.clear();
+ expand_type = _('a');
+ goto matched_keyword;
+ }
+
+ }
+ }
+ }
+ }
+ // Remainder of the string
+ result += s; s.clear();
+
+ matched_keyword:;
+ }
+
+ return result;
}
diff --git a/src/data/keyword.hpp b/src/data/keyword.hpp
index 946f4bfe..aa5fd187 100644
--- a/src/data/keyword.hpp
+++ b/src/data/keyword.hpp
@@ -16,6 +16,8 @@
DECLARE_POINTER_TYPE(KeywordParam);
DECLARE_POINTER_TYPE(KeywordExpansion);
DECLARE_POINTER_TYPE(KeywordMode);
+DECLARE_POINTER_TYPE(Keyword);
+class KeywordTrie;
// ----------------------------------------------------------------------------- : Keyword components
@@ -24,8 +26,7 @@ class KeywordParam {
public:
String name; ///< Name of the parameter type
String description; ///< Description of the type
- String match; ///< Uncompiled regex
- wxRegEx matchRe; ///< Regular expression to match
+ String match; ///< Regular expression to match
OptionalScript script; ///< Transformation of the value for showing in the reminder text
String example; ///< Example for preview dialog
@@ -49,9 +50,18 @@ class KeywordExpansion {
public:
String match; ///< String to match, tags are used for parameters
vector parameters; ///< The types of parameters
-// wxRegEx splitter; ///< Regular expression to split/match the components, automatically generated
+ wxRegEx matchRe; ///< Regular expression to match and split parameters, automatically generated
StringScript reminder; ///< Reminder text of the keyword
- String mode; ///< Mode of use, can be used by scripts (only gives the name). Default is the mode of the Keyword.
+ String mode; ///< Mode of use, can be used by scripts (only gives the name)
+// . Default is the mode of the Keyword.
+ String regex;//TODO REMOVE
+
+ /// Prepare the expansion: (re)generate matchRe and the list of parameters.
+ /** Throws when there is an error in the input
+ * @param param_types A list of all parameter types.
+ * @param force Re-prepare even if the regex¶meters are okay
+ */
+ void prepare(const vector& param_types, bool force = false);
DECLARE_REFLECTION();
};
@@ -64,7 +74,7 @@ class Keyword {
String keyword; ///< The keyword
vector expansions; ///< Expansions, i.e. ways to use this keyword
String rules; ///< Rules/explanation
- String mode; ///< Mode of use, can be used by scripts (only gives the name)
+// String mode; ///< Mode of use, can be used by scripts (only gives the name)
DECLARE_REFLECTION();
};
@@ -72,21 +82,41 @@ class Keyword {
// ----------------------------------------------------------------------------- : Using keywords
-/// A class that allows for fast matching of keywords
-class KeywordDatabase;
-DECLARE_POINTER_TYPE(KeywordDatabase);
-
-/// Create a new keyword database
-KeywordDatabaseP new_keyword_database();
-
-/// Add a keyword to a KeywordDatabase
+/// A database of keywords to allow for fast matching
/** NOTE: keywords may not be altered after they are added to the database,
* The database should be rebuild.
*/
-void add_keyword(KeywordDatabase& db, const Keyword& kw);
-
-/// Expand/update all keywords in the given string
-String expand_keywords(const KeywordDatabase& db, const String& text);
+class KeywordDatabase {
+ public:
+ KeywordDatabase();
+ ~KeywordDatabase();
+
+ /// Add a list of keywords to be matched
+ void add(const vector&);
+ /// Add a keyword to be matched
+ void add(const Keyword&);
+ /// Add an expansion of a keyword to be matched
+ void add(const KeywordExpansion&);
+
+ /// Prepare the parameters and match regex for a list of keywords
+ static void prepare_parameters(const vector&, const vector&);
+ static void prepare_parameters(const vector&, const Keyword&);
+
+ /// Clear the database
+ void clear();
+ /// Is the database empty?
+ inline bool empty() const { return !root; }
+
+ /// Expand/update all keywords in the given string.
+ /** @param expand_default script function indicating whether reminder text should be shown by default
+ * @param combine_script script function to combine keyword and reminder text in some way
+ * @param ctx context for evaluation of scripts
+ */
+ String expand(const String& text, const ScriptValueP& expand_default, const ScriptValueP& combine_script, Context& ctx) const;
+
+ private:
+ KeywordTrie* root; ///< Data structure for finding keywords
+};
// ----------------------------------------------------------------------------- : EOF
#endif
diff --git a/src/data/set.cpp b/src/data/set.cpp
index 6183cdcd..5437a99c 100644
--- a/src/data/set.cpp
+++ b/src/data/set.cpp
@@ -19,7 +19,7 @@
#include
DECLARE_TYPEOF_COLLECTION(CardP);
-DECLARE_TYPEOF_NO_REV((IndexMap));
+DECLARE_TYPEOF_NO_REV(IndexMap);
// ----------------------------------------------------------------------------- : Set
diff --git a/src/data/set.hpp b/src/data/set.hpp
index 336bf834..4009a9da 100644
--- a/src/data/set.hpp
+++ b/src/data/set.hpp
@@ -14,6 +14,7 @@
#include
#include
#include // for Set::value
+#include
#include
DECLARE_POINTER_TYPE(Card);
@@ -57,6 +58,7 @@ class Set : public Packaged {
vector keywords; ///< Additional keywords used in this set
String apprentice_code; ///< Code to use for apprentice (Magic only)
ActionStack actions; ///< Actions performed on this set and the cards in it
+ KeywordDatabase keyword_db; ///< Database for matching keywords, must be cleared when keywords change
/// A context for performing scripts
/** Should only be used from the main thread! */
diff --git a/src/gui/control/card_list.cpp b/src/gui/control/card_list.cpp
index 177e76f6..2a667963 100644
--- a/src/gui/control/card_list.cpp
+++ b/src/gui/control/card_list.cpp
@@ -26,8 +26,8 @@
DECLARE_TYPEOF_COLLECTION(CardP);
DECLARE_TYPEOF_COLLECTION(FieldP);
DECLARE_POINTER_TYPE(ChoiceValue);
-DECLARE_TYPEOF((map));
-DECLARE_TYPEOF_NO_REV((IndexMap));
+DECLARE_TYPEOF(map);
+DECLARE_TYPEOF_NO_REV(IndexMap);
// ----------------------------------------------------------------------------- : Events
diff --git a/src/gui/control/graph.cpp b/src/gui/control/graph.cpp
index 1bdaadd3..8ef0bea1 100644
--- a/src/gui/control/graph.cpp
+++ b/src/gui/control/graph.cpp
@@ -15,7 +15,7 @@ DECLARE_TYPEOF_COLLECTION(GraphAxisP);
DECLARE_TYPEOF_COLLECTION(GraphElementP);
DECLARE_TYPEOF_COLLECTION(GraphGroup);
DECLARE_TYPEOF_COLLECTION(int);
-DECLARE_TYPEOF((map));
+DECLARE_TYPEOF(map);
// ----------------------------------------------------------------------------- : Events
diff --git a/src/gui/control/native_look_editor.cpp b/src/gui/control/native_look_editor.cpp
index 09689f0a..891a2604 100644
--- a/src/gui/control/native_look_editor.cpp
+++ b/src/gui/control/native_look_editor.cpp
@@ -12,7 +12,7 @@
#include
DECLARE_TYPEOF_COLLECTION(ValueViewerP);
-DECLARE_TYPEOF_NO_REV((IndexMap));
+DECLARE_TYPEOF_NO_REV(IndexMap);
// ----------------------------------------------------------------------------- : NativeLookEditor
diff --git a/src/gui/value/text.cpp b/src/gui/value/text.cpp
index 07f03f56..3ea7f7c8 100644
--- a/src/gui/value/text.cpp
+++ b/src/gui/value/text.cpp
@@ -570,7 +570,7 @@ void TextValueEditor::fixSelection(IndexType t, Movement dir) {
// start and end must be on the same side of separators
size_t seppos = val.find(_(" seppos) {
// not on same side, move selection end before sep
selection_end = index_to_cursor(val, seppos, dir);
diff --git a/src/render/card/viewer.cpp b/src/render/card/viewer.cpp
index 6e346fc8..5743da36 100644
--- a/src/render/card/viewer.cpp
+++ b/src/render/card/viewer.cpp
@@ -18,7 +18,7 @@
#include // clearDC
DECLARE_TYPEOF_COLLECTION(ValueViewerP);
-DECLARE_TYPEOF_NO_REV((IndexMap));
+DECLARE_TYPEOF_NO_REV(IndexMap);
// ----------------------------------------------------------------------------- : DataViewer
diff --git a/src/resource/msw/mse.rc b/src/resource/msw/mse.rc
index 25790471..4470fffd 100644
--- a/src/resource/msw/mse.rc
+++ b/src/resource/msw/mse.rc
@@ -110,7 +110,7 @@ bool_no IMAGE "../common/bool_no.png"
//help_page BITMAP "help_page.png"
about IMAGE "../common/about.png"
-two_beta IMAGE "../common/two_beta.png"
+two_beta IMAGE "../common/two_beta.png"
btn_normal IMAGE "../common/btn_normal.png"
btn_hover IMAGE "../common/btn_hover.png"
btn_focus IMAGE "../common/btn_focus.png"
@@ -155,7 +155,7 @@ FILETYPE VFT_APP
VALUE "License", "GNU General Public License 2 or later; This is free software, and you are welcome to redistribute it under certain conditions; See the help file for details"
VALUE "FileDescription", "Magic Set Editor"
VALUE "InternalName", "mse2/8"
- VALUE "LegalCopyright", " 2001-2006 Twan van Laarhoven"
+ VALUE "LegalCopyright", "© 2001-2006 Twan van Laarhoven"
VALUE "ProductName", "Magic Set Editor"
}
}
diff --git a/src/script/context.cpp b/src/script/context.cpp
index 3c98d3f4..8631dec2 100644
--- a/src/script/context.cpp
+++ b/src/script/context.cpp
@@ -215,7 +215,7 @@ void instrUnary (UnaryInstructionType i, ScriptValueP& a) {
a = toScript(-(int)*a);
break;
case I_NOT:
- a = toScript(!(int)*a);
+ a = toScript(!(bool)*a);
break;
}
}
diff --git a/src/script/functions.cpp b/src/script/functions.cpp
index 1a592a6b..7d2e7a26 100644
--- a/src/script/functions.cpp
+++ b/src/script/functions.cpp
@@ -12,6 +12,7 @@
#include
#include
#include
+#include
#include
#include
@@ -265,7 +266,7 @@ String spec_sort(const String& spec, const String& input) {
ScriptValueP ScriptRule_##funname::eval(Context& ctx) const
// Utility for defining a script rule with two parameters
-#define SCRIPT_RULE_2(funname, type1, name1, type2, name2) \
+#define SCRIPT_RULE_2_N(funname, type1, str1, name1, type2, str2, name2) \
class ScriptRule_##funname: public ScriptValue { \
public: \
inline ScriptRule_##funname(const type1& name1, const type2& name2) \
@@ -278,16 +279,18 @@ String spec_sort(const String& spec, const String& input) {
type2 name2; \
}; \
SCRIPT_FUNCTION(funname##_rule) { \
- SCRIPT_PARAM(type1, name1); \
- SCRIPT_PARAM(type2, name2); \
+ SCRIPT_PARAM_N(type1, str1, name1); \
+ SCRIPT_PARAM_N(type2, str2, name2); \
return new_intrusive2(name1, name2); \
} \
SCRIPT_FUNCTION(funname) { \
- SCRIPT_PARAM(type1, name1); \
- SCRIPT_PARAM(type2, name2); \
+ SCRIPT_PARAM_N(type1, str1, name1); \
+ SCRIPT_PARAM_N(type2, str2, name2); \
return ScriptRule_##funname(name1, name2).eval(ctx); \
} \
ScriptValueP ScriptRule_##funname::eval(Context& ctx) const
+#define SCRIPT_RULE_2(funname, type1, name1, type2, name2) \
+ SCRIPT_RULE_2_N(funname, type1, _(#name1), name1, type2, _(#name2), name2)
// Create a rule for spec_sorting strings
@@ -339,10 +342,21 @@ SCRIPT_FUNCTION(contains) {
SCRIPT_RETURN(input.find(match) != String::npos);
}
-SCRIPT_FUNCTION(format) {
- SCRIPT_PARAM(String, format);
- SCRIPT_PARAM(String, input);
- SCRIPT_RETURN(format_string(_("%") + replace_all(format, _("%"), _("")), input));
+SCRIPT_RULE_1(format, String, format) {
+ String fmt = _("%") + replace_all(format, _("%"), _(""));
+ // determine type expected by format string
+ if (format.find_first_of(_("DdIiOoXx")) != String.npos) {
+ SCRIPT_PARAM(int, input);
+ SCRIPT_RETURN(String::Format(fmt, input));
+ } else if (format.find_first_of(_("EeFfGg")) != String.npos) {
+ SCRIPT_PARAM(double, input);
+ SCRIPT_RETURN(String::Format(fmt, input));
+ } else if (format.find_first_of(_("Ss")) != String.npos) {
+ SCRIPT_PARAM(String, input);
+ SCRIPT_RETURN(format_string(fmt, input));
+ } else {
+ throw ScriptError(_ERROR_1_("unsupported format", format));
+ }
}
// ----------------------------------------------------------------------------- : Tagged stuff
@@ -381,7 +395,23 @@ SCRIPT_RULE_1(tag_remove, String, tag) {
SCRIPT_RETURN(remove_tag(input, tag));
}
-// ----------------------------------------------------------------------------- : Vector stuff
+// ----------------------------------------------------------------------------- : Keywords
+
+SCRIPT_RULE_2_N(expand_keywords, ScriptValueP, _("default expand"), default_expand,
+ ScriptValueP, _("combine"), combine) {
+ SCRIPT_PARAM(String, input);
+ SCRIPT_PARAM(Set*, set);
+ KeywordDatabase& db = set->keyword_db;
+ if (db.empty()) {
+ db.add(set->game->keywords);
+ db.add(set->keywords);
+ db.prepare_parameters(set->game->keyword_parameter_types, set->game->keywords);
+ db.prepare_parameters(set->game->keyword_parameter_types, set->keywords);
+ }
+ SCRIPT_RETURN(db.expand(input, default_expand, combine, ctx));
+}
+
+// ----------------------------------------------------------------------------- : Collection stuff
/// compare script values for equallity
bool equal(const ScriptValue& a, const ScriptValue& b) {
@@ -497,7 +527,7 @@ SCRIPT_FUNCTION_DEP(combined_editor) {
size_t pos = value.find(_("type() == SCRIPT_INT) {
- return (int)*value; // boolean up-to-dateness from parameter
+ return (bool)*value; // boolean up-to-dateness from parameter
} else {
return true;
}
diff --git a/src/script/script_manager.cpp b/src/script/script_manager.cpp
index a0b64d79..60132965 100644
--- a/src/script/script_manager.cpp
+++ b/src/script/script_manager.cpp
@@ -22,8 +22,8 @@ DECLARE_TYPEOF(Contexts);
DECLARE_TYPEOF_COLLECTION(CardP);
DECLARE_TYPEOF_COLLECTION(FieldP);
DECLARE_TYPEOF_COLLECTION(Dependency);
-DECLARE_TYPEOF_NO_REV((IndexMap));
-DECLARE_TYPEOF_NO_REV((IndexMap));
+DECLARE_TYPEOF_NO_REV(IndexMap);
+DECLARE_TYPEOF_NO_REV(IndexMap);
// initialize functions, from functions.cpp
void init_script_functions(Context& ctx);
diff --git a/src/script/to_value.hpp b/src/script/to_value.hpp
index c23a496e..636f7b5a 100644
--- a/src/script/to_value.hpp
+++ b/src/script/to_value.hpp
@@ -53,13 +53,13 @@ class ScriptCollection : public ScriptValue {
public:
inline ScriptCollection(const Collection* v) : value(v) {}
virtual ScriptType type() const { return SCRIPT_COLLECTION; }
- virtual String typeName() const { return _("collection"); }
+ virtual String typeName() const { return _TYPE_("collection"); }
virtual ScriptValueP getMember(const String& name) const {
long index;
if (name.ToLong(&index) && index >= 0 && (size_t)index < value->size()) {
return toScript(value->at(index));
} else {
- throw ScriptError(_("Collection has no member ") + name);
+ return ScriptValue::getMember(name);
}
}
virtual ScriptValueP makeIterator() const {
@@ -79,7 +79,7 @@ ScriptValueP get_member(const map& m, const String& name) {
if (it != m.end()) {
return toScript(it->second);
} else {
- throw ScriptError(_ERROR_1_("collection has no member", name));
+ throw ScriptError(_ERROR_2_("has no member", _TYPE_("collection"), name));
}
}
@@ -89,7 +89,7 @@ ScriptValueP get_member(const IndexMap& m, const String& name) {
if (it != m.end()) {
return toScript(*it);
} else {
- throw ScriptError(_ERROR_1_("collection has no member", name));
+ throw ScriptError(_ERROR_2_("has no member", _TYPE_("collection"), name));
}
}
@@ -99,7 +99,7 @@ class ScriptMap : public ScriptValue {
public:
inline ScriptMap(const Collection* v) : value(v) {}
virtual ScriptType type() const { return SCRIPT_COLLECTION; }
- virtual String typeName() const { return _("collection"); }
+ virtual String typeName() const { return _TYPE_("collection"); }
virtual ScriptValueP getMember(const String& name) const {
return get_member(*value, name);
}
@@ -132,7 +132,7 @@ class ScriptObject : public ScriptValue {
public:
inline ScriptObject(const T& v) : value(v) {}
virtual ScriptType type() const { return SCRIPT_OBJECT; }
- virtual String typeName() const { return _("object"); }
+ virtual String typeName() const { return _TYPE_("object"); }
virtual operator String() const { ScriptValueP d = getDefault(); return d ? *d : ScriptValue::operator String(); }
virtual operator double() const { ScriptValueP d = getDefault(); return d ? *d : ScriptValue::operator double(); }
virtual operator int() const { ScriptValueP d = getDefault(); return d ? *d : ScriptValue::operator int(); }
@@ -147,7 +147,7 @@ class ScriptObject : public ScriptValue {
if (d) {
return d->getMember(name);
} else {
- throw ScriptError(_ERROR_1_("object has no member", name));
+ throw ScriptValue::getMember(name);
}
}
}
@@ -239,7 +239,9 @@ inline ScriptValueP toScript(const Defaultable& v) { return toScript(v()); }
* Throws an error if the parameter is not found.
*/
#define SCRIPT_PARAM(Type, name) \
- Type name = getParam(ctx.getVariable(_(#name)))
+ SCRIPT_PARAM_N(Type, _(#name), name)
+#define SCRIPT_PARAM_N(Type, str, name) \
+ Type name = getParam(ctx.getVariable(str))
template
inline T getParam (const ScriptValueP& value) {
diff --git a/src/script/value.cpp b/src/script/value.cpp
index 7396aff1..7e8a079f 100644
--- a/src/script/value.cpp
+++ b/src/script/value.cpp
@@ -15,14 +15,14 @@
// Base cases
ScriptValue::operator String() const { return _("[[") + typeName() + _("]]"); }
-ScriptValue::operator int() const { throw ScriptError( _("Can't convert from ")+typeName()+_(" to integer number")); }
-ScriptValue::operator double() const { throw ScriptError( _("Can't convert from ")+typeName()+_(" to real number" )); }
-ScriptValue::operator Color() const { throw ScriptError( _("Can't convert from ")+typeName()+_(" to color" )); }
-ScriptValueP ScriptValue::eval(Context&) const { throw ScriptError( _("Can't convert from ")+typeName()+_(" to function" )); }
-ScriptValueP ScriptValue::getMember(const String& name) const { throw ScriptError(typeName() + _(" has no member '") + name + _("'")); }
+ScriptValue::operator int() const { throw ScriptError(_ERROR_2_("can't convert", typeName(), _TYPE_("integer" ))); }
+ScriptValue::operator double() const { throw ScriptError(_ERROR_2_("can't convert", typeName(), _TYPE_("real" ))); }
+ScriptValue::operator Color() const { throw ScriptError(_ERROR_2_("can't convert", typeName(), _TYPE_("color" ))); }
+ScriptValueP ScriptValue::eval(Context&) const { throw ScriptError(_ERROR_2_("can't convert", typeName(), _TYPE_("function"))); }
+ScriptValueP ScriptValue::getMember(const String& name) const { throw ScriptError(_ERROR_2_("has no member", typeName(), name)); }
ScriptValueP ScriptValue::next() { throw InternalError(_("Can't convert from ")+typeName()+_(" to iterator")); }
-ScriptValueP ScriptValue::makeIterator() const { throw ScriptError( _("Can't convert from ")+typeName()+_(" to collection")); }
-int ScriptValue::itemCount() const { throw ScriptError( _("Can't convert from ")+typeName()+_(" to collection")); }
+ScriptValueP ScriptValue::makeIterator() const { throw ScriptError(_ERROR_2_("can't convert", typeName(), _TYPE_("collection"))); }
+int ScriptValue::itemCount() const { throw ScriptError(_ERROR_2_("can't convert", typeName(), _TYPE_("collection"))); }
ScriptValueP ScriptValue::dependencyMember(const String& name, const Dependency&) const { return dependency_dummy; }
ScriptValueP ScriptValue::dependencies(Context&, const Dependency&) const { return dependency_dummy; }
@@ -63,7 +63,7 @@ class ScriptInt : public ScriptValue {
public:
ScriptInt(int v) : value(v) {}
virtual ScriptType type() const { return SCRIPT_INT; }
- virtual String typeName() const { return _("integer number"); }
+ virtual String typeName() const { return _TYPE_("integer"); }
virtual operator String() const { return String() << value; }
virtual operator double() const { return value; }
virtual operator int() const { return value; }
@@ -108,7 +108,7 @@ class ScriptBool : public ScriptValue {
public:
ScriptBool(bool v) : value(v) {}
virtual ScriptType type() const { return SCRIPT_INT; }
- virtual String typeName() const { return _("boolean"); }
+ virtual String typeName() const { return _TYPE_("boolean"); }
virtual operator String() const { return value ? _("true") : _("false"); }
virtual operator int() const { return value; }
private:
@@ -130,7 +130,7 @@ class ScriptDouble : public ScriptValue {
public:
ScriptDouble(double v) : value(v) {}
virtual ScriptType type() const { return SCRIPT_DOUBLE; }
- virtual String typeName() const { return _("real number"); }
+ virtual String typeName() const { return _TYPE_("real"); }
virtual operator String() const { return String() << value; }
virtual operator double() const { return value; }
virtual operator int() const { return (int)value; }
@@ -149,14 +149,14 @@ class ScriptString : public ScriptValue {
public:
ScriptString(const String& v) : value(v) {}
virtual ScriptType type() const { return SCRIPT_STRING; }
- virtual String typeName() const { return _("string"); }
+ virtual String typeName() const { return _TYPE_("string"); }
virtual operator String() const { return value; }
virtual operator double() const {
double d;
if (value.ToDouble(&d)) {
return d;
} else {
- throw ScriptError(_("Not a number: '") + value + _("'"));
+ throw ScriptError(_ERROR_3_("can't convert value", value, typeName(), _TYPE_("double")));
}
}
virtual operator int() const {
@@ -168,7 +168,7 @@ class ScriptString : public ScriptValue {
} else if (value == _("no") || value == _("false") || value.empty()) {
return false;
} else {
- throw ScriptError(_("Not a number: '") + value + _("'"));
+ throw ScriptError(_ERROR_3_("can't convert value", value, typeName(), _TYPE_("integer")));
}
}
virtual operator Color() const {
@@ -176,7 +176,7 @@ class ScriptString : public ScriptValue {
if (wxSscanf(value.c_str(),_("rgb(%u,%u,%u)"),&r,&g,&b)) {
return Color(r, g, b);
} else {
- throw ScriptError(_("Not a color: '") + value + _("'"));
+ throw ScriptError(_ERROR_3_("can't convert value", value, typeName(), _TYPE_("color")));
}
}
virtual int itemCount() const { return (int)value.size(); }
@@ -186,7 +186,7 @@ class ScriptString : public ScriptValue {
if (name.ToLong(&index) && index >= 0 && (size_t)index < value.size()) {
return toScript(String(1,value[index]));
} else {
- throw ScriptError(_("String \"") + value + _("\" has no member ") + name);
+ throw ScriptError(_ERROR_2_("has no member value", value, name));
}
}
private:
@@ -205,7 +205,7 @@ class ScriptColor : public ScriptValue {
public:
ScriptColor(const Color& v) : value(v) {}
virtual ScriptType type() const { return SCRIPT_COLOR; }
- virtual String typeName() const { return _("color"); }
+ virtual String typeName() const { return _TYPE_("color"); }
virtual operator Color() const { return value; }
virtual operator String() const {
return String::Format(_("rgb(%u,%u,%u)"), value.Red(), value.Green(), value.Blue());
@@ -225,7 +225,7 @@ ScriptValueP toScript(const Color& v) {
class ScriptNil : public ScriptValue {
public:
virtual ScriptType type() const { return SCRIPT_NIL; }
- virtual String typeName() const { return _("nil"); }
+ virtual String typeName() const { return _TYPE_("nil"); }
virtual operator String() const { return wxEmptyString; }
virtual operator double() const { return 0.0; }
virtual operator int() const { return 0; }
diff --git a/src/script/value.hpp b/src/script/value.hpp
index 03ffa38e..52977d6b 100644
--- a/src/script/value.hpp
+++ b/src/script/value.hpp
@@ -48,6 +48,8 @@ class ScriptValue : public IntrusivePtrBase {
virtual operator double() const;
/// Convert this value to an integer
virtual operator int() const;
+ /// Convert this value to a boolean
+ inline operator bool() const { return (int)*this; }
/// Convert this value to a color
virtual operator Color() const;
diff --git a/src/util/for_each.hpp b/src/util/for_each.hpp
index 527972d4..19ad20db 100644
--- a/src/util/for_each.hpp
+++ b/src/util/for_each.hpp
@@ -85,12 +85,19 @@
}
/// Declare typeof magic for a specific std::vector type
- #define DECLARE_TYPEOF_COLLECTION(T) DECLARE_TYPEOF(vector); \
-// DECLARE_TYPEOF_CONST(set)
+ #define DECLARE_TYPEOF_COLLECTION(T) DECLARE_TYPEOF(vector< T >); \
+ DECLARE_TYPEOF_CONST(set< T >)
#endif
-
+/// Use for template classes
+/** i.e.
+ * DECLARE_TYPEOF(pair);
+ * instead of
+ * DECLARE_TYPEOF(pair);
+ */
+#define COMMA ,
+
// ----------------------------------------------------------------------------- : Looping macros with iterators
/// Iterate over a collection, using an iterator it of type Type
diff --git a/src/util/locale.hpp b/src/util/locale.hpp
index 6bc6596e..64ddd07a 100644
--- a/src/util/locale.hpp
+++ b/src/util/locale.hpp
@@ -97,8 +97,9 @@ String tr(const SymbolFont&, const String& key, const String& def);
inline String format_string(const String& format, ...) {
va_list args;
va_start(args, format);
- return String::Format(format, args);
+ String res = String::Format(format, args);
va_end(args);
+ return res;
}
inline String format_string(const String& format, const String& a0) {
return String::Format(format, a0.c_str());
diff --git a/src/util/platform.hpp b/src/util/platform.hpp
index 59ccdd4d..2f3fdb67 100644
--- a/src/util/platform.hpp
+++ b/src/util/platform.hpp
@@ -9,7 +9,7 @@
/** @file util/platform.hpp
*
- * @brief Platform specific hacks
+ * @brief Platform and compiler specific hacks.
*/
// ----------------------------------------------------------------------------- : Includes
@@ -23,11 +23,22 @@
#ifdef __linux__
+ /// wxMkDir as documented
inline void wxMkDir(const String& dir) {
wxMkDir(wxConvLocal.cWX2MB(dir), 0777);
}
#endif
+// ----------------------------------------------------------------------------- : GCC
+
+#ifdef __GNUC__
+
+ /// Absolute value of integers
+ template
+ inline T abs(T a) { return a < 0 ? -a : a; }
+
+#endif
+
// ----------------------------------------------------------------------------- : EOF
#endif
diff --git a/src/util/string.hpp b/src/util/string.hpp
index 873b6cd3..f26253de 100644
--- a/src/util/string.hpp
+++ b/src/util/string.hpp
@@ -113,7 +113,8 @@ String capitalize_sentence(const String&);
String cannocial_name_form(const String&);
/// Returns the singular form of a string
-/** Used for reflection, for example "vector apples" is written with keys "apple"
+/** Used for reflection, for example "vector apples" is written with keys
+ * singular_form("apples"), which is "apple"
*/
String singular_form(const String&);
diff --git a/src/util/tagged_string.cpp b/src/util/tagged_string.cpp
index e4ebcf8b..48e28e92 100644
--- a/src/util/tagged_string.cpp
+++ b/src/util/tagged_string.cpp
@@ -121,6 +121,10 @@ size_t match_close_tag(const String& str, size_t start) {
return String::npos;
}
+size_t match_close_tag_end(const String& str, size_t start) {
+ return skip_tag(str, match_close_tag(str, start));
+}
+
size_t last_start_tag_before(const String& str, const String& tag, size_t start) {
start = min(str.size(), start);
for (size_t pos = start ; pos > 0 ; --pos) {
@@ -178,7 +182,7 @@ size_t index_to_cursor(const String& str, size_t index, Movement dir) {
if (is_substr(str, i, _(" before && index < after) {
// index is inside an atom, determine on which side we want the cursor
if (dir == MOVE_RIGHT) {
@@ -215,7 +219,7 @@ void cursor_to_index_range(const String& str, size_t cursor, size_t& start, size
// a tag
if (is_substr(str, i, _("