6
\$\begingroup\$

I want to create a system that marks up text with tags inserted into a stream. Each tag frames a part of the text, creating a tree-like structure, just as in classic markup languages (HTML, XML...). I want to externalize the formatting of the text according to the tags. I've found a way of doing this that works, but as I'm not comfortable with std::streams in C++, I would appreciate advice and a code review for this project.

#include <iostream>
#include <sstream>
#include <vector>
using namespace std;

enum class Tag{
  no_tag,
  p,
  span,
  it,
  em
};

struct Part{
    Part(Tag tag=Tag::no_tag, const std::string& s=""):tag(tag), s(s){}

    Tag tag;
    std::string s;
};

struct FormatterBase{
    virtual string format(const Tag tag, const string& s)const=0;
    virtual ~FormatterBase(){}
};

struct Formatter : public FormatterBase{
    string format(Tag tag, const string& s)const override{
        switch(tag){
        case Tag::no_tag : 
            //std::cout << "format no_tag " << s << "\n";
            return s;
        case Tag::p : 
            //std::cout << "format p " << s << "\n";
            return s + "\n";
        case Tag::span : 
            //std::cout << "format span " << s << "\n";
            return "<" + s + ">";
        case Tag::strong : 
            //std::cout << "format it" << s << "\n";
            return "*" + s + "*";
        case Tag::em : 
            //std::cout << "format em " << s << "\n";
            return "**" + s + "**";
        }
    }
};

struct MarkupStream : public ostream{

    MarkupStream(const Formatter& fm, Tag tag=Tag::no_tag)
        :_fm(fm)
        ,_parts({{tag, ""}}){}

    template<class T>
    MarkupStream& write(T& text){
        stringstream s;
        s << text ;
        _parts.back().s += s.str();
        return *this;
    }
    
    MarkupStream& concat(MarkupStream& stream){
        _parts.back().s += stream.release_last_part();
        return *this;
    }
    
    MarkupStream& operator() (MarkupStream& stream){
        return concat(stream);
    }
    
    MarkupStream& operator ()(Tag tag){
        _parts.emplace_back(tag, "");
        return *this;
    }
    
    string release_last_part(){
        auto& last = _parts.back();
        auto res = _fm.format(last.tag, last.s);
        _parts.pop_back();
        return res;
    }

    const Formatter& _fm;
    vector<Part> _parts;
    
};

template<class T>
MarkupStream& operator << (MarkupStream& stream, T text){
  return stream.write(text);
}

MarkupStream& operator << (MarkupStream& lf, MarkupStream& rg){
  return lf.concat(rg);
}

int main() { 
  Formatter fm;
  MarkupStream s(fm);
  s(Tag::p) << "Hello World! " << (s(Tag::span)(s(Tag::it) << 1 << 2)) << " " << (s(Tag::em) << "hi!");
  cout << s.release_last_part();
  return 0;
}

Here is the output of this code:

Hello World! <12> hi!

\$\endgroup\$

2 Answers 2

6
\$\begingroup\$

About your API

To actually use your code, you have to write quite a few lines of code just to get one line printed. Consider your main() function:

Formatter fm;
MarkupStream s(fm);
s(Tag::p) << "Hello World! " << (s(Tag::span)(s(Tag::it) << 1 << 2)) << " " << (s(Tag::em) << "hi!");
cout << s.release_last_part();

Here you need to know about Formatter, MarkupStream and several Tag::*s, and after builting the formatted stream, you have to call release_last_part() to get the formatted string out. Consider this with how you'd use std::format():

std::cout << std::format("Hello World! <*{}{}*> **{}**\n", 1, 2, "hi!");

Of course, that hardcodes how paragraphs, spans, italics and emphasis are formatted. Ideally you'd want something that is as close to the simplicity of std::format, while having the option of a custom formatter. You could for example make your own function that can parse a format string and delegate rendering to another function:

template<typename... Args>
std::string format_markup(FormatterBase& formatter, std::string_view format_string, Args&&... args) {
    …
}

int main() {
    Formatter fm{};
    std::cout << format_markup(fm, "Hello World! <*{}{}*> **{}**\n", 1, 2, "hi!");
}

Of course, you then have to come up with your own format string syntax. You could reuse <, * and **, so that you have a markdown-like syntax. But you could create a HTMLFormatter then which turns that into HTML tags.

You can also define custom formatters for std::format, but since std::format doesn't handle nesting of things to be formatted, I think that is not the way to go.

Don't forget to escape strings containing markup

Consider writing:

s(Tag::p) << "The result of the expression "
          << (s(Tag::strong) << "3*3")
          << " is 9.";

Since there is a single * in the text you want to make strong, the output will be rendered incorrectly. So you need to look for markup in the strings you are trying to mark up, and escape those markup characters somehow.

MarkupStream() should take a reference to FormatterBase

The first parameter of MarkupStream() is Formatter&, but that prevents you from creating another overload of FormatterBase and using that. Make sure the first parameter is of type FormatterBase&.

It's not very efficient

A lot of memory allocations and string copies are made by your code. It's fine if you render the occasional marked up text, but if you use it a lot it might become a performance bottleneck. Ideally you avoid storing data as much as possible, but due to the nesting you still have to store intermediate results. Still, try to std::move strings where possible. You can also avoid the std::vector<Part>: consider that you only call pop_back() once before returning the formatted std::string.

You don't have to use operator<<

While we know operator<< from C++'s stream-based IO functions, I'm not sure they are a good fit for what you are trying to do. It requires you to write a lot of parentheses, making the code using it resemble LISP more than it does C++. What if you made MarkupStream::operator() take more parameters? Consider being able to write:

using enum Tag;
std::cout << s(p, "Hello World! ", s(span, s(it, 1, 2)), " ", s(em, "hi!"));

So operator() takes a tag as the first parameter, then an arbitrary amount of strings that it will concatenate before formatting the result of that using the formatter, and then it will return the formatted result as a std::string.

Consider further separating marking up from formatting

One issue I see with code like s(Tag::foo) is that it combines the markup tags with the stream that does the formatting. What if you could first build a marked up string, and then pass that to something that does the actual formatting? Your code could them look like:

auto marked_up_text = P("Hello World! ", Span(It(1, 2)), " ", Em("hi!"));
Formatter fm{};
std::cout << format_markup(fm, marked_up_text);

Where P, Span, It and Em are now classes that store a tag and a list of their arguments, allowing format_markup() to traverse the list recursively, formatting everything using the passed in FormatterBase.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for this relevant feedback, I think I'll opt for the comma notation, which is very pretty. \$\endgroup\$ Commented May 24 at 7:29
4
\$\begingroup\$

Avoid using namespace std;

std namespace is a quite large namespace. This behavior completely obviates the benefits of namespaces because it brings all its names into the global namespace. Please check What's the problem with "using namespace std;"? and make a habit of using the (very short) namespace prefix std::.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.