diff options
Diffstat (limited to 'src/format.cc')
-rw-r--r-- | src/format.cc | 488 |
1 files changed, 488 insertions, 0 deletions
diff --git a/src/format.cc b/src/format.cc new file mode 100644 index 00000000..f26a86a1 --- /dev/null +++ b/src/format.cc @@ -0,0 +1,488 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <system.hh> + +#include "format.h" +#include "scope.h" +#include "pstream.h" + +namespace ledger { + +format_t::elision_style_t format_t::default_style = TRUNCATE_TRAILING; +bool format_t::default_style_changed = false; + +void format_t::element_t::dump(std::ostream& out) const +{ + out << "Element: "; + + switch (type) { + case STRING: out << " STRING"; break; + case EXPR: out << " EXPR"; break; + } + + out << " flags: 0x" << std::hex << int(flags()); + out << " min: "; + out << std::right; + out.width(2); + out << std::dec << int(min_width); + out << " max: "; + out << std::right; + out.width(2); + out << std::dec << int(max_width); + + switch (type) { + case STRING: + out << " str: '" << boost::get<string>(data) << "'" << std::endl; + break; + case EXPR: + out << " expr: " << boost::get<expr_t>(data) << std::endl; + break; + } +} + +namespace { + expr_t parse_single_expression(const char *& p, bool single_expr = true) + { + string temp(p); + ptristream str(const_cast<char *&>(p)); + expr_t expr; + expr.parse(str, single_expr ? PARSE_SINGLE : PARSE_PARTIAL, temp); + if (str.eof()) { + expr.set_text(p); + p += std::strlen(p); + } else { + assert(str.good()); + istream_pos_type pos = str.tellg(); + expr.set_text(string(p, p + long(pos))); + p += long(pos) - 1; + + // Don't gobble up any whitespace + const char * base = p; + while (p >= base && std::isspace(*p)) + p--; + } + return expr; + } +} + +format_t::element_t * format_t::parse_elements(const string& fmt, + const optional<format_t&>& tmpl) +{ + std::auto_ptr<element_t> result; + + element_t * current = NULL; + + char buf[1024]; + char * q = buf; + + for (const char * p = fmt.c_str(); *p; p++) { + if (*p != '%' && *p != '\\') { + *q++ = *p; + continue; + } + + if (! result.get()) { + result.reset(new element_t); + current = result.get(); + } else { + current->next.reset(new element_t); + current = current->next.get(); + } + + if (q != buf) { + current->type = element_t::STRING; + current->data = string(buf, q); + q = buf; + + current->next.reset(new element_t); + current = current->next.get(); + } + + if (*p == '\\') { + p++; + current->type = element_t::STRING; + switch (*p) { + case 'b': current->data = string("\b"); break; + case 'f': current->data = string("\f"); break; + case 'n': current->data = string("\n"); break; + case 'r': current->data = string("\r"); break; + case 't': current->data = string("\t"); break; + case 'v': current->data = string("\v"); break; + case '\\': current->data = string("\\"); break; + default: current->data = string(1, *p); break; + } + continue; + } + + ++p; + while (*p == '-') { + switch (*p) { + case '-': + current->add_flags(ELEMENT_ALIGN_LEFT); + break; + } + ++p; + } + + std::size_t num = 0; + while (*p && std::isdigit(*p)) { + num *= 10; + num += *p++ - '0'; + } + current->min_width = num; + + if (*p == '.') { + ++p; + num = 0; + while (*p && std::isdigit(*p)) { + num *= 10; + num += *p++ - '0'; + } + current->max_width = num; + if (current->min_width == 0) + current->min_width = current->max_width; + } + + switch (*p) { + case '%': + current->type = element_t::STRING; + current->data = string("%"); + break; + + case '$': { + if (! tmpl) + throw_(format_error, _("Prior field reference, but no template")); + + p++; + if (*p == '0' || (! std::isdigit(*p) && + *p != 'A' && *p != 'B' && *p != 'C' && + *p != 'D' && *p != 'E' && *p != 'F')) + throw_(format_error, _("%$ field reference must be a digit from 1-9")); + + unsigned int index = std::isdigit(*p) ? *p - '0' : (*p - 'A' + 10); + element_t * tmpl_elem = tmpl->elements.get(); + + for (unsigned int i = 1; i < index && tmpl_elem; i++) { + tmpl_elem = tmpl_elem->next.get(); + while (tmpl_elem && tmpl_elem->type != element_t::EXPR) + tmpl_elem = tmpl_elem->next.get(); + } + + if (! tmpl_elem) + throw_(format_error, _("%$ reference to a non-existent prior field")); + + *current = *tmpl_elem; + break; + } + + case '(': + case '{': { + bool format_amount = *p == '{'; + if (format_amount) p++; + + current->type = element_t::EXPR; + current->data = parse_single_expression(p, ! format_amount); + + // Wrap the subexpression in calls to justify and scrub + if (format_amount) { + if (! *p || *(p + 1) != '}') + throw_(format_error, _("Expected closing brace")); + else + p++; + + expr_t::ptr_op_t op = boost::get<expr_t>(current->data).get_op(); + + expr_t::ptr_op_t amount_op; + expr_t::ptr_op_t colorize_op; + if (op->kind == expr_t::op_t::O_CONS) { + amount_op = op->left(); + colorize_op = op->right(); + } else { + amount_op = op; + } + + expr_t::ptr_op_t scrub_node(new expr_t::op_t(expr_t::op_t::IDENT)); + scrub_node->set_ident("scrub"); + + expr_t::ptr_op_t call1_node(new expr_t::op_t(expr_t::op_t::O_CALL)); + call1_node->set_left(scrub_node); + call1_node->set_right(amount_op); + + expr_t::ptr_op_t arg1_node(new expr_t::op_t(expr_t::op_t::VALUE)); + expr_t::ptr_op_t arg2_node(new expr_t::op_t(expr_t::op_t::VALUE)); + expr_t::ptr_op_t arg3_node(new expr_t::op_t(expr_t::op_t::VALUE)); + + arg1_node->set_value(current->min_width > 0 ? + long(current->min_width) : -1); + arg2_node->set_value(current->max_width > 0 ? + long(current->max_width) : -1); + arg3_node->set_value(! current->has_flags(ELEMENT_ALIGN_LEFT)); + + current->min_width = 0; + current->max_width = 0; + + expr_t::ptr_op_t args1_node(new expr_t::op_t(expr_t::op_t::O_CONS)); + args1_node->set_left(arg2_node); + args1_node->set_right(arg3_node); + + expr_t::ptr_op_t args2_node(new expr_t::op_t(expr_t::op_t::O_CONS)); + args2_node->set_left(arg1_node); + args2_node->set_right(args1_node); + + expr_t::ptr_op_t args3_node(new expr_t::op_t(expr_t::op_t::O_CONS)); + args3_node->set_left(call1_node); + args3_node->set_right(args2_node); + + expr_t::ptr_op_t seq1_node(new expr_t::op_t(expr_t::op_t::O_SEQ)); + seq1_node->set_left(args3_node); + + expr_t::ptr_op_t justify_node(new expr_t::op_t(expr_t::op_t::IDENT)); + justify_node->set_ident("justify"); + + expr_t::ptr_op_t call2_node(new expr_t::op_t(expr_t::op_t::O_CALL)); + call2_node->set_left(justify_node); + call2_node->set_right(seq1_node); + + string prev_expr = boost::get<expr_t>(current->data).text(); + + if (colorize_op) { + expr_t::ptr_op_t ansify_if_node(new expr_t::op_t(expr_t::op_t::IDENT)); + ansify_if_node->set_ident("ansify_if"); + + expr_t::ptr_op_t args4_node(new expr_t::op_t(expr_t::op_t::O_CONS)); + args4_node->set_left(call2_node); + args4_node->set_right(colorize_op); + + expr_t::ptr_op_t seq2_node(new expr_t::op_t(expr_t::op_t::O_SEQ)); + seq2_node->set_left(args4_node); + + expr_t::ptr_op_t call3_node(new expr_t::op_t(expr_t::op_t::O_CALL)); + call3_node->set_left(ansify_if_node); + call3_node->set_right(seq2_node); + + current->data = expr_t(call3_node); + } else { + current->data = expr_t(call2_node); + } + + boost::get<expr_t>(current->data).set_text(prev_expr); + } + break; + } + + default: + throw_(format_error, _("Unrecognized formatting character: %1") << *p); + } + } + + if (q != buf) { + if (! result.get()) { + result.reset(new element_t); + current = result.get(); + } else { + current->next.reset(new element_t); + current = current->next.get(); + } + current->type = element_t::STRING; + current->data = string(buf, q); + } + + return result.release(); +} + +string format_t::real_calc(scope_t& scope) +{ + std::ostringstream out_str; + + for (element_t * elem = elements.get(); elem; elem = elem->next.get()) { + std::ostringstream out; + string name; + + if (elem->has_flags(ELEMENT_ALIGN_LEFT)) + out << std::left; + else + out << std::right; + + switch (elem->type) { + case element_t::STRING: + if (elem->min_width > 0) + out.width(elem->min_width); + out << boost::get<string>(elem->data); + break; + + case element_t::EXPR: { + expr_t& expr(boost::get<expr_t>(elem->data)); + try { + + expr.compile(scope); + + value_t value; + if (expr.is_function()) { + call_scope_t args(scope); + args.push_back(long(elem->max_width)); + value = expr.get_function()(args); + } else { + value = expr.calc(scope); + } + DEBUG("format.expr", "value = (" << value << ")"); + + if (elem->min_width > 0) + value.print(out, static_cast<int>(elem->min_width), -1, + ! elem->has_flags(ELEMENT_ALIGN_LEFT)); + else + out << value.to_string(); + } + catch (const calc_error&) { + add_error_context(_("While calculating format expression:")); + add_error_context(expr.context_to_str()); + throw; + } + break; + } + + default: + assert(false); + break; + } + + if (elem->max_width > 0 || elem->min_width > 0) { + unistring temp(out.str()); + string result; + + if (elem->max_width > 0 && elem->max_width < temp.length()) { + result = truncate(temp, elem->max_width); + } else { + result = temp.extract(); + if (elem->min_width > temp.length()) + for (std::size_t i = 0; i < elem->min_width - temp.length(); i++) + result += " "; + } + out_str << result; + } else { + out_str << out.str(); + } + } + + return out_str.str(); +} + +string format_t::truncate(const unistring& ustr, + const std::size_t width, + const std::size_t account_abbrev_length) +{ + assert(width < 4095); + + const std::size_t len = ustr.length(); + if (width == 0 || len <= width) + return ustr.extract(); + + std::ostringstream buf; + + elision_style_t style = default_style; + if (account_abbrev_length > 0 && ! default_style_changed) + style = ABBREVIATE; + + switch (style) { + case TRUNCATE_LEADING: + // This method truncates at the beginning. + buf << ".." << ustr.extract(len - (width - 2), width - 2); + break; + + case TRUNCATE_MIDDLE: + // This method truncates in the middle. + buf << ustr.extract(0, (width - 2) / 2) + << ".." + << ustr.extract(len - ((width - 2) / 2 + (width - 2) % 2), + (width - 2) / 2 + (width - 2) % 2); + break; + + case ABBREVIATE: + if (account_abbrev_length > 0) { + std::list<string> parts; + string::size_type beg = 0; + string strcopy(ustr.extract()); + for (string::size_type pos = strcopy.find(':'); + pos != string::npos; + beg = pos + 1, pos = strcopy.find(':', beg)) + parts.push_back(string(strcopy, beg, pos - beg)); + parts.push_back(string(strcopy, beg)); + + std::ostringstream result; + + std::size_t newlen = len; + for (std::list<string>::iterator i = parts.begin(); + i != parts.end(); + i++) { + // Don't contract the last element + std::list<string>::iterator x = i; + if (++x == parts.end()) { + result << *i; + break; + } + + if (newlen > width) { + unistring temp(*i); + if (temp.length() > account_abbrev_length) { + result << temp.extract(0, account_abbrev_length) << ":"; + newlen -= temp.length() - account_abbrev_length; + } else { + result << temp.extract() << ":"; + newlen -= temp.length(); + } + } else { + result << *i << ":"; + } + } + + if (newlen > width) { + // Even abbreviated its too big to show the last account, so + // abbreviate all but the last and truncate at the beginning. + unistring temp(result.str()); + assert(temp.length() > width - 2); + buf << ".." << temp.extract(temp.length() - (width - 2), width - 2); + } else { + buf << result.str(); + } + break; + } + // fall through... + + case TRUNCATE_TRAILING: + // This method truncates at the end (the default). + buf << ustr.extract(0, width - 2) << ".."; + break; + } + + return buf.str(); +} + +} // namespace ledger |