Commit be2222ca authored by twanvl's avatar twanvl

Partially working text editor

parent b108304f
......@@ -14,6 +14,7 @@
#include <data/field/color.hpp>
#include <data/field/image.hpp>
#include <data/field/symbol.hpp>
#include <util/tagged_string.hpp>
// ----------------------------------------------------------------------------- : ValueAction
......@@ -35,16 +36,14 @@ class SimpleValueAction : public ValueAction {
swap(static_cast<T&>(*valueP).*member, new_value);
}
virtual bool merge(const Action* action) {
virtual bool merge(const Action& action) {
if (!ALLOW_MERGE) return false;
if (const SimpleValueAction* sva = dynamic_cast<const SimpleValueAction*>(action)) {
if (sva->valueP == valueP) {
TYPE_CASE(action, SimpleValueAction) {
if (action.valueP == valueP) {
// adjacent actions on the same value, discard the other one,
// because it only keeps an intermediate value
return true;
}
} else {
return false;
}
return false;
}
......@@ -60,3 +59,85 @@ ValueAction* value_action(const SymbolValueP& value, const FileName&
// ----------------------------------------------------------------------------- : Text
TextValueAction::TextValueAction(const TextValueP& value, size_t start, size_t end, size_t new_end, const Defaultable<String>& new_value, const String& name)
: ValueAction(value), new_value(new_value), name(name)
, selection_start(start), selection_end(end), new_selection_end(new_end)
{}
String TextValueAction::getName(bool to_undo) const { return name; }
void TextValueAction::perform(bool to_undo) {
swap(value().value, new_value);
// if (value().value.age < new_value.age) value().value.age = Age();
swap(selection_end, new_selection_end);
}
bool TextValueAction::merge(const Action& action) {
TYPE_CASE(action, TextValueAction) {
if (&action.value() == &value() && action.name == name && action.selection_start == selection_end) {
// adjacent edits, keep old value of this, it is older
selection_end = action.selection_end;
return true;
}
}
return false;
}
TextValue& TextValueAction::value() const {
return static_cast<TextValue&>(*valueP);
}
TextValueAction* toggle_format_action(const TextValueP& value, const String& tag, size_t start, size_t end, const String& action_name) {
if (start > end) swap(start, end);
String new_value;
const String& str = value->value();
// Are we inside the tag we are toggling?
size_t tagpos = in_tag(str, _("<") + tag, start, end);
if (tagpos == String::npos) {
// we are not inside this tag, add it
new_value = str.substr(0, start);
new_value += _("<") + tag + _(">");
new_value += str.substr(start, end - start);
new_value += _("</") + tag + _(">");
new_value += str.substr(end);
} else {
// we are inside this tag, _('remove') it
new_value = str.substr(0, start);
new_value += _("</") + tag + _(">");
new_value += str.substr(start, end - start);
new_value += _("<") + tag + _(">");
new_value += str.substr(end);
}
// Build action
if (start != end) {
// don't simplify if start == end, this way we insert <b></b>, allowing the
// user to press Ctrl+B and start typing bold text
new_value = simplify_tagged(new_value);
}
if (value->value() == new_value) {
return nullptr; // no changes
} else {
return new TextValueAction(value, start, end, end, new_value, action_name);
}
}
TextValueAction* typing_action(const TextValueP& value, size_t start, size_t end, const String& replacement, const String& action_name) {
bool reverse = start > end;
if (reverse) swap(start, end);
String new_value = tagged_substr_replace(value->value(), start, end, replacement);
if (value->value() == new_value) {
// no change
return nullptr;
} else {
// if (name == _("Backspace")) {
// // HACK: put start after end
if (reverse) {
return new TextValueAction(value, end, start, start+replacement.size(), new_value, action_name);
} else {
return new TextValueAction(value, start, end, start+replacement.size(), new_value, action_name);
}
}
}
......@@ -37,18 +37,9 @@ class ValueAction : public Action {
const ValueP valueP; ///< The modified value
};
/// Utility macro for declaring classes derived from ValueAction
#define DECLARE_VALUE_ACTION(Type) \
protected: \
inline Type##Value& value() const { \
return static_cast<Type##Value&>(*valueP); \
} \
public: \
virtual void perform(bool to_undo)
// ----------------------------------------------------------------------------- : Simple
/// Action that updates a Value to a new value
ValueAction* value_action(const ChoiceValueP& value, const Defaultable<String>& new_value);
ValueAction* value_action(const ColorValueP& value, const Defaultable<Color>& new_value);
ValueAction* value_action(const ImageValueP& value, const FileName& new_value);
......@@ -56,17 +47,30 @@ ValueAction* value_action(const SymbolValueP& value, const FileName&
// ----------------------------------------------------------------------------- : Text
/*
class ColorValueAction : public ValueAction {
/// An action that changes a TextValue
class TextValueAction : public ValueAction {
public:
ColorValueAction(const ColorValueP& value, const Defaultable<Color>& color);
TextValueAction(const TextValueP& value, size_t start, size_t end, size_t new_end, const Defaultable<String>& new_value, const String& name);
DECLARE_VALUE_ACTION(Color);
virtual String getName(bool to_undo) const;
virtual void perform(bool to_undo);
virtual bool merge(const Action& action);
/// The modified selection
size_t selection_start, selection_end;
private:
Defaultable<Color> color; ///< The new/old color
inline TextValue& value() const;
size_t new_selection_end;
Defaultable<String> new_value;
String name;
};
*/
/// Action for toggleing some formating tag on or off in some range
TextValueAction* toggle_format_action(const TextValueP& value, const String& tag, size_t start, size_t end, const String& action_name);
/// Typing in a TextValue, replace the selection [start...end) with replacement
TextValueAction* typing_action(const TextValueP& value, size_t start, size_t end, const String& replacement, const String& action_name);
// ----------------------------------------------------------------------------- : EOF
#endif
......@@ -11,6 +11,7 @@
#include <util/prec.hpp>
#include <util/defaultable.hpp>
#include <util/rotation.hpp>
#include <data/field.hpp>
#include <data/font.hpp>
#include <data/symbol_font.hpp>
......@@ -68,6 +69,11 @@ class TextStyle : public Style {
virtual bool update(Context&);
virtual void initDependencies(Context&, const Dependency&) const;
/// The rotation to use when drawing
inline Rotation getRotation() const {
return Rotation(angle, getRect());
}
private:
DECLARE_REFLECTION();
};
......
......@@ -76,6 +76,7 @@ class SymbolFont : public Packaged {
/// Size of a single symbol
RealSize symbolSize (Context& ctx, double font_size, const DrawableSymbol& sym);
public:
/// Size of the default symbol
RealSize defaultSymbolSize(Context& ctx, double font_size);
......
......@@ -12,6 +12,7 @@
#include <data/field.hpp>
#include <data/stylesheet.hpp>
#include <data/settings.hpp>
#include <wx/caret.h>
DECLARE_TYPEOF_COLLECTION(ValueViewerP);
DECLARE_TYPEOF_COLLECTION(ValueViewer*);
......@@ -22,7 +23,10 @@ DataEditor::DataEditor(Window* parent, int id, long style)
: CardViewer(parent, id, style)
, current_viewer(nullptr)
, current_editor(nullptr)
{}
{
// Create a caret
SetCaret(new wxCaret(this,1,1));
}
ValueViewerP DataEditor::makeViewer(const StyleP& style) {
return style->makeEditor(*this, style);
......
This diff is collapsed.
......@@ -13,14 +13,116 @@
#include <gui/value/editor.hpp>
#include <render/value/text.hpp>
class TextValueEditorScrollBar;
// ----------------------------------------------------------------------------- : TextValueEditor
/// Directions of cursor movement
enum Movement
{ MOVE_LEFT ///< Always move the cursor to the left
, MOVE_MID ///< Move in whichever direction the distance to move is shorter (TODO: define shorter)
, MOVE_RIGHT ///< Always move the cursor to the right
};
/// An editor 'control' for editing TextValues
/** Okay, this class responds to pretty much every event available... :)
*/
class TextValueEditor : public TextValueViewer, public ValueEditor {
public:
DECLARE_VALUE_EDITOR(Text);
~TextValueEditor();
// --------------------------------------------------- : Events
virtual void onFocus();
virtual void onLoseFocus();
virtual void onLeftDown (const RealPoint& pos, wxMouseEvent&);
virtual void onLeftUp (const RealPoint& pos, wxMouseEvent&);
virtual void onLeftDClick(const RealPoint& pos, wxMouseEvent&);
virtual void onRightDown (const RealPoint& pos, wxMouseEvent&);
virtual void onMotion (const RealPoint& pos, wxMouseEvent&);
virtual void onMouseWheel(const RealPoint& pos, wxMouseEvent& ev);
virtual bool onContextMenu(wxMenu& m, wxContextMenuEvent&);
virtual void onMenu(wxCommandEvent&);
virtual void onChar(wxKeyEvent&);
// --------------------------------------------------- : Actions
virtual void onValueChange();
virtual void onAction(const ValueAction&, bool undone);
// --------------------------------------------------- : Clipboard
virtual bool canCopy() const;
virtual bool canPaste() const;
virtual bool doCopy();
virtual bool doPaste();
virtual bool doDelete();
// --------------------------------------------------- : Formating
virtual bool canFormat(int type) const;
virtual bool hasFormat(int type) const;
virtual void doFormat(int type);
// --------------------------------------------------- : Selection
virtual void select(size_t start, size_t end);
virtual size_t selectionStart() const { return selection_start; }
virtual size_t selectionEnd() const { return selection_end; }
// --------------------------------------------------- : Other
virtual wxCursor cursor() const;
virtual void determineSize();
virtual void onShow(bool);
// --------------------------------------------------- : Data
private:
size_t selection_start, selection_end; ///< Cursor position/selection (if any)
TextValueEditorScrollBar* scrollbar; ///< Scrollbar for multiline fields in native look
// --------------------------------------------------- : Selection / movement
/// Move the selection to a new location, clears the previously drawn selection
void moveSelection(size_t new_end, bool also_move_start=true, Movement dir = MOVE_MID);
/// Move the selection to a new location, but does not redraw
void moveSelectionNoRedraw(size_t new_end, bool also_move_start=true, Movement dir = MOVE_MID);
/// Replace the current selection with 'replacement', name the action
void replaceSelection(const String& replacement, const String& name);
/// Make sure the selection satisfies its constraints
/** - selection_start and selection_end are inside the text
* - not inside tags
* - the selection does not contain a <sep> or </sep> tag
*
* When correcting the selection, move in the given direction
*/
void fixSelection(Movement dir = MOVE_MID);
/// Return a position resulting from moving pos outside the range [start...end), in the direction dir
static size_t move(size_t pos, size_t start, size_t end, Movement dir);
/// Move the caret to the selection_end position and show it
void showCaret();
/// Position of previous visible & selectable character
size_t prevCharBoundry(size_t pos) const;
size_t nextCharBoundry(size_t pos) const;
/// Front of previous word, used witch Ctrl+Left/right
size_t prevWordBoundry(size_t pos) const;
size_t nextWordBoundry(size_t pos) const;
// --------------------------------------------------- : Scrolling
friend class TextValueEditorScrollBar;
// virtual void determineSize();
/// Scroll to the given position, called by scrollbar
void scrollTo(int pos);
};
// ----------------------------------------------------------------------------- : EOF
......
......@@ -32,7 +32,7 @@ struct TextViewer::Line {
/// Index just beyond the last character on this line
size_t end() const { return start + positions.size() - 1; }
/// Find the index of the character at the given position on this line
/** Always returns a value in the range [start..end()) */
/** Always returns a value in the range [start..end()] */
size_t posToIndex(double x) const;
/// Is this line visible using the given rectangle?
......@@ -47,13 +47,12 @@ struct TextViewer::Line {
size_t TextViewer::Line::posToIndex(double x) const {
// largest index with pos <= x
vector<double>::const_iterator it1 = lower_bound(positions.begin(), positions.end(), x);
if (it1 == positions.end()) return end();
vector<double>::const_iterator it2 = lower_bound(positions.begin(), positions.end(), x);
if (it2 == positions.begin()) return start;
// first index with pos > x
vector<double>::const_iterator it2 = it1 + 1;
if (it2 == positions.end()) return it1 - positions.begin();
if (x - *it1 <= *it2 - x) return it1 - positions.begin(); // it1 is closer
else return it2 - positions.begin(); // it2 is closer
vector<double>::const_iterator it1 = it2 - 1;
if (x - *it1 <= *it2 - x) return it1 - positions.begin() + start; // it1 is closer
else return it2 - positions.begin() + start; // it2 is closer
}
// ----------------------------------------------------------------------------- : TextViewer
......@@ -65,7 +64,7 @@ TextViewer::~TextViewer() {}
// ----------------------------------------------------------------------------- : Drawing
void TextViewer::draw(RotatedDC& dc, const String& text, const TextStyle& style, Context& ctx, DrawWhat what) {
Rotater r(dc, Rotation(style.angle, style.getRect()));
Rotater r(dc, style.getRotation());
if (lines.empty()) {
// not prepared yet
prepareElements(text, style, ctx);
......@@ -81,7 +80,7 @@ void TextViewer::draw(RotatedDC& dc, const String& text, const TextStyle& style,
}
void TextViewer::drawSelection(RotatedDC& dc, const TextStyle& style, size_t sel_start, size_t sel_end) {
Rotater r(dc, Rotation(style.angle, style.getRect()));
Rotater r(dc, style.getRotation());
if (sel_start == sel_end) return;
if (sel_end < sel_start) swap(sel_start, sel_end);
dc.SetBrush(*wxBLACK_BRUSH);
......@@ -109,6 +108,25 @@ void TextViewer::reset() {
// ----------------------------------------------------------------------------- : Positions
const TextViewer::Line& TextViewer::findLine(size_t index) const {
FOR_EACH_CONST(l, lines) {
if (l.end() > index) return l;
}
return lines.front();
}
size_t TextViewer::moveLine(size_t index, int delta) const {
const Line* line1 = &findLine(index);
const Line* line2 = line1 + delta;
if (line2 >= &lines.front() && line2 <= &lines.back()) {
size_t idx = index - line1->start;
if (idx < 0 || idx >= line1->positions.size()) return index; // can't move
return line2->posToIndex(line1->positions[idx]); // character at the same position
} else {
return index; // can't move
}
}
size_t TextViewer::lineStart(size_t index) const {
if (lines.empty()) return 0;
return findLine(index).start;
......@@ -119,11 +137,32 @@ size_t TextViewer::lineEnd(size_t index) const {
return findLine(index).end();
}
const TextViewer::Line& TextViewer::findLine(size_t index) const {
FOR_EACH_CONST(l, lines) {
if (l.end() > index) return l;
struct CompareTop {
inline bool operator () (const TextViewer::Line& l, double y) const { return l.top < y; }
inline bool operator () (double y, const TextViewer::Line& l) const { return y < l.top; }
};
size_t TextViewer::indexAt(const RealPoint& pos) const {
// 1. find the line
vector<Line>::const_iterator l = lower_bound(lines.begin(), lines.end(), pos.y, CompareTop());
if (l != lines.begin()) l--;
assert(l != lines.end());
// 2. find char on line
return l->posToIndex(pos.x);
}
RealRect TextViewer::charRect(size_t index) const {
const Line& l = findLine(index);
size_t pos = index - l.start;
if (pos >= l.positions.size()) {
return RealRect(l.positions.back(), l.top, 0, l.line_height);
} else {
return RealRect(l.positions[pos], l.top, l.positions[pos + 1] - l.positions[pos], l.line_height);
}
return lines.front();
}
double TextViewer::heightOfLastLine() const {
if (lines.empty()) return 0;
else return lines.back().line_height;
}
// ----------------------------------------------------------------------------- : Elements
......
......@@ -56,16 +56,32 @@ class TextViewer {
// --------------------------------------------------- : Positions
/// Find the character index that is before the given index, and which has a nonzero width
size_t moveLeft(size_t index) const;
/// Find the character index that is before/after the given index, and which has a nonzero width
// size_t moveChar(size_t index, int delta) const;
/// Find the character index that is on a line above/below index
/** If this would move outisde the text, returns the input index */
size_t moveLine(size_t index, int delta) const;
/// The character index of the start of the line that character #index is on
size_t lineStart(size_t index) const;
/// The character index past the end of the line that character #index is on
size_t lineEnd (size_t index) const;
/// Find the index of the character at the given position
/** If the position is before everything returns 0,
* if it is after everything returns text.size().
* The position is in internal coordinates */
size_t indexAt(const RealPoint& pos) const;
/// Find the position of the character at the given index
/** The position is in internal coordinates */
RealPoint posOf(size_t index) const;
/// Return the rectangle around a single character
RealRect charRect(size_t index) const;
/// Return the height of the last line
double heightOfLastLine() const;
private:
// --------------------------------------------------- : More drawing
double scale; /// < Scale when drawing
......
......@@ -25,7 +25,7 @@ class TextValueViewer : public ValueViewer {
virtual void onValueChange();
virtual void onStyleChange();
private:
protected:
TextViewer v;
};
......
......@@ -41,7 +41,7 @@ RealRect ValueViewer::boundingBox() const {
void ValueViewer::drawFieldBorder(RotatedDC& dc) {
if (viewer.drawBorders() && getField()->editable) {
dc.SetPen(viewer.borderPen(viewer.focusedViewer() == this));
dc.SetPen(viewer.borderPen(isCurrent()));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(styleP->getRect().grow(dc.trInvS(1)));
}
......@@ -50,6 +50,9 @@ void ValueViewer::drawFieldBorder(RotatedDC& dc) {
bool ValueViewer::nativeLook() const {
return viewer.nativeLook();
}
bool ValueViewer::isCurrent() const {
return viewer.focusedViewer() == this;
}
// ----------------------------------------------------------------------------- : Type dispatch
......
......@@ -33,7 +33,7 @@ void ActionStack::add(Action* action, bool allow_merge) {
FOR_EACH(a, redo_actions) delete a;
redo_actions.clear();
// try to merge?
if (allow_merge && !undo_actions.empty() && undo_actions.back()->merge(action)) {
if (allow_merge && !undo_actions.empty() && undo_actions.back()->merge(*action)) {
// merged with top undo action
delete action;
} else {
......
......@@ -40,7 +40,7 @@ class Action {
/** Either: return false and do nothing
* Or: return true and change this action to incorporate both actions
*/
virtual bool merge(const Action* action) { return false; }
virtual bool merge(const Action& action) { return false; }
};
// ----------------------------------------------------------------------------- : Action listeners
......
......@@ -73,9 +73,12 @@ class Rotation {
friend class Rotater;
public:
/// Is the rotation sideways (90 or 270 degrees)?
// Note: angle & 2 == 0 for angle in {0, 180} and != 0 for angle in {90, 270)
inline bool sideways() const { return (angle & 2) != 0; }
protected:
/// Is the x axis 'reversed' (after turning sideways)?
inline bool revX() const { return angle >= 180; }
/// Is the y axis 'reversed' (after turning sideways)?
......
......@@ -62,6 +62,10 @@ String trim_left(const String& s) {
}
}
String substr_replace(const String& input, size_t start, size_t end, const String& replacement) {
return input.substr(0,start) + replacement + input.substr(end);
}
// ----------------------------------------------------------------------------- : Words
String last_word(const String& s) {
......
......@@ -81,6 +81,9 @@ String trim(const String&);
/// Remove whitespace from the start of a string
String trim_left(const String&);
/// Replace the substring [start...end) of 'input' with 'replacement'
String substr_replace(const String& input, size_t start, size_t end, const String& replacement);
// ----------------------------------------------------------------------------- : Words
/// Returns the last word in a string
......
......@@ -7,6 +7,7 @@
// ----------------------------------------------------------------------------- : Includes
#include <util/tagged_string.hpp>
#include <stack>
// ----------------------------------------------------------------------------- : Conversion to/from normal string
......@@ -41,6 +42,44 @@ String escape(const String& str) {
return ret;
}
String fix_old_tags(const String& str) {
String ret; ret.reserve(str.size());
stack<String> tags;
bool intag = false;
// invariant : intag => !tags.empty()
for (size_t i = 0 ; i < str.size() ; ++i) {
Char c = str.GetChar(i);
if (is_substr(str, i, _("</>"))) {
i += 2;
// old style close tag, replace by the correct tag type
if (!tags.empty()) {
// need a close tag?
if (!tags.top().empty()) {
ret += _("</") + tags.top() + _(">");
}
tags.pop();
}
intag = false;
} else {
ret += c;
if (c==_('<')) {
intag = true;
tags.push(wxEmptyString);
} else if (c==_('>') && intag) {
intag = false;
if (!starts_with(tags.top(), _("kw")) && !starts_with(tags.top(), _("atom"))) {
// only keep keyword related stuff
ret.resize(ret.size() - tags.top().size() - 2); // remove from output
tags.top() = wxEmptyString;
}
} else if (intag) {
tags.top() += c;
}
}
}
return ret;
}
// ----------------------------------------------------------------------------- : Finding tags
size_t skip_tag(const String& str, size_t start) {
......@@ -49,5 +88,179 @@ size_t skip_tag(const String& str, size_t start) {
return end == String::npos ? String::npos : end + 1;
}
size_t match_close_tag(const String& str, size_t start) {
String tag = tag_type_at(str, start);
String ctag = _("/") + tag;
size_t size = str.size();
int taglevel = 1;
for (size_t pos = start + tag.size() + 2 ; pos < size ; ++pos) {
Char c = str.GetChar(pos);
if (c == _('<')) {
if (is_substr(str, pos + 1, tag)) {
++taglevel;
pos += tag.size() + 1;
} else if (is_substr(str, pos + 1, ctag)) {
--taglevel; // close tag
if (taglevel == 0) return pos;
pos += ctag.size() + 1;
}
}
}
return String::npos;
}
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) {
if (is_substr(str, pos - 1, tag)) {
return pos - 1;
}
}
return String::npos;
}
size_t in_tag(const String& str, const String& tag, size_t start, size_t end) {
if (start > end) swap(start, end);
size_t pos = last_start_tag_before(str, tag, start);
if (pos == String::npos) return String::npos; // no tag found before start
size_t posE = match_close_tag(str, pos);
if (posE < end) return String::npos; // the tag ends before end
return pos;
}
String tag_at(const String& str, size_t pos) {
size_t end = str.find_first_of(_(">"), pos);
if (end == String::npos) return wxEmptyString;
return str.substr(pos + 1, end - pos - 1);
}
String tag_type_at(const String& str, size_t pos) {
size_t end = str.find_first_of(_(">-"), pos);
if (end == String::npos) return wxEmptyString;
return str.substr(pos + 1, end - pos - 1);
}
String close_tag(const String& tag) {
if (tag.size() < 1) return _("</>");
else return _("</") + tag.substr(1);
}
String anti_tag(const String& tag) {
if (!tag.empty() && tag.GetChar(0) == _('/')) return _("<") + tag.substr(1) + _(">");
else return _("</") + tag + _(">");
}
// ----------------------------------------------------------------------------- : Global operations
// ----------------------------------------------------------------------------- : Updates
/// Return all open or close tags in the given range from a string
/** for example:
* if close_tags == false, "text<tag>text</tag>text" --> "<tag>"
* if close_tags == true, "text<tag>text</tag>text" --> "</tag>"
*/
String get_tags(const String& str, size_t start, size_t end, bool close_tags) {
String ret;
bool intag = false;
bool keeptag = false;
for (size_t i = start ; i < end ; ++i) {
Char c = str.GetChar(i);
if (c == _('<') && !intag) {
intag = true;
// is this tag an open tag?
if (i + 1 < end && (str.GetChar(i + 1) == _('/')) == close_tags) {
keeptag = true;
}
}
if (intag && keeptag) ret += c;
if (c == _('>')) intag = false;
}
return ret;
}
String tagged_substr_replace(const String& input, size_t start, size_t end, const String& replacement) {
assert(start <= end);
size_t size = input.size();
String ret; ret.reserve(size + replacement.size() - (end - start)); // estimated size
return simplify_tagged(
substr_replace(input, start, end,
get_tags(input, start, end, true) + // close tags
escape(replacement) +
get_tags(input, start, end, false) // open tags
));
}
// ----------------------------------------------------------------------------- : Simplification
String simplify_tagged(const String& str) {
return simplify_tagged_overlap(simplify_tagged_merge(str));
}
// Add a tag to a stack of tags, try to cancel it out
// If </tag> is in stack remove it and returns true
// otherwise appends <tag> and returns fales
// (where </tag> is the negation of tag)
bool add_or_cancel_tag(const String& tag, String& stack) {
String anti = anti_tag(tag);
size_t pos = stack.find(anti);
if (pos == String::npos) {
stack += _("<") + tag + _(">");
return false;
} else {
// cancel out with anti tag
stack = stack.substr(0, pos) + stack.substr(pos + anti.size());
return true;
}
}
String simplify_tagged_merge(const String& str) {
String ret; ret.reserve(str.size());
String waiting_tags; // tags that are waiting to be written to the output
size_t size = str.size();
for (size_t i = 0 ; i < size ; ++i) {
Char c = str.GetChar(i);
if (c == _('<')) {
String tag = tag_at(str, i);
add_or_cancel_tag(tag, waiting_tags);
i += tag.size() + 1;
} else {
ret += waiting_tags;
waiting_tags.clear();
ret += c;
}
}
return ret + waiting_tags;
}
String simplify_tagged_overlap(const String& str) {
String ret; ret.reserve(str.size());
String open_tags; // tags we are in
size_t size = str.size();
for (size_t i = 0 ; i < size ; ++i) {
Char c = str.GetChar(i);
if (c == _('<')) {
String tag = tag_at(str, i);
if (starts_with(tag, _("b")) || starts_with(tag, _("i")) || starts_with(tag, _("sym")) ||
starts_with(tag, _("/b")) || starts_with(tag, _("/i")) || starts_with(tag, _("/sym"))) {
// optimize this tag
if (open_tags.find(_("<") + tag + _(">")) == String::npos) {
// we are not already inside this tag
add_or_cancel_tag(tag, open_tags);
if (open_tags.find(anti_tag(tag)) != String::npos) {
// still not canceled out
i += tag.size() + 2;
continue;
}
} else {
// skip this tag, doubling it has no effect
i += tag.size() + 2;
add_or_cancel_tag(tag, open_tags);
continue;
}
}
}
ret += c;
}
return ret;
}
......@@ -46,10 +46,17 @@ String fix_old_tags(const String&);
size_t skip_tag(const String& str, size_t start);
/// Find the position of the closing tag matching the tag at start
/** If not found returns String::npos
*/
/** If not found returns String::npos */
size_t match_close_tag(const String& str, size_t start);
/// Find the last start tag before position start
/** If not found returns String::npos */
size_t last_start_tag_before(const String& str, const String& tag, size_t start);
/// Is the given range entirely contained in a given tag?
/** If so: return the start position of that tag, otherwise returns String::npos */
size_t in_tag(const String& str, const String& tag, size_t start, size_t end);
/// Return the tag at the given position (without the <>)
String tag_at(const String& str, size_t pos);
......@@ -80,6 +87,17 @@ String remove_tag_exact(const String& str, const String& tag);
*/
String remove_tag_contents(const String& str, const String& tag);
// ----------------------------------------------------------------------------- : Updates
/// 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.
* This means that when removing "<x>a</x>" nothing is left,
* but with input "<x>a" -> "<x>" and "</>a" -> "</>".
* Escapes the replacement, i.e. all < in become \1.
*/
String tagged_substr_replace(const String& input, size_t start, size_t end, const String& replacement);
// ----------------------------------------------------------------------------- : Simplification
/// Verify that a string is correctly tagged, if it is not, change it so it is
......@@ -95,10 +113,14 @@ String verify_tagged(const String& str);
*/
String simplify_tagged(const String& str);
/// Simplify a tagged string by merging adjecent open/close tags "<tag></tag>" --> ""
/// Simplify a tagged string by merging adjecent open/close tags
/** e.g. "<tag></tag>" --> ""
*/
String simplify_tagged_merge(const String& str);
/// Simplify overlapping formatting tags
/** e.g. "<i>blah<i>blah</i>blah</i>" -> "<i>blahblahblah</i>"
*/
String simplify_tagged_overlap(const String& str);
// ----------------------------------------------------------------------------- : EOF
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment