mirror of
https://github.com/ultimatepp/ultimatepp.git
synced 2026-05-16 14:16:09 -06:00
601 lines
15 KiB
C++
601 lines
15 KiB
C++
#include "RichText.h"
|
|
|
|
namespace Upp {
|
|
|
|
extern Color (*QTFColor[])();
|
|
extern int QTFFontHeight[];
|
|
|
|
void SeparateNumber(String& s)
|
|
{
|
|
if(*s.Last() >= '0' && *s.Last() <= '9')
|
|
s.Cat(';');
|
|
}
|
|
|
|
String QtfFormat(Color c)
|
|
{
|
|
if(IsNull(c)) return "N";
|
|
for(int i = 0; i < 10; i++)
|
|
if((*QTFColor[i])() == c)
|
|
return String(i + '0', 1);
|
|
if(c.GetR() == c.GetG() && c.GetG() == c.GetB())
|
|
return Sprintf("(%d)", c.GetR());
|
|
return Sprintf("(%d.%d.%d)", c.GetR(), c.GetG(), c.GetB());
|
|
}
|
|
|
|
void LngFmt(String& fmt, dword l, dword lang)
|
|
{
|
|
if(lang != (dword)l) {
|
|
if(l == 0)
|
|
fmt << "%-";
|
|
else
|
|
if(l == LNG_ENGLISH)
|
|
fmt << "%%";
|
|
else
|
|
fmt << "%" << LNGAsText(l);
|
|
}
|
|
}
|
|
|
|
void CharFmt(String& fmt, const RichPara::CharFormat& a, const RichPara::CharFormat& b)
|
|
{
|
|
if(a.IsBold() != b.IsBold()) fmt.Cat('*');
|
|
if(a.IsItalic() != b.IsItalic()) fmt.Cat('/');
|
|
if(a.IsUnderline() != b.IsUnderline()) fmt.Cat('_');
|
|
if(a.IsStrikeout() != b.IsStrikeout()) fmt.Cat('-');
|
|
if(a.IsNonAntiAliased() != b.IsNonAntiAliased()) fmt.Cat('t');
|
|
if(a.capitals != b.capitals) fmt.Cat('c');
|
|
if(a.dashed != b.dashed) fmt.Cat('d');
|
|
if(a.sscript != b.sscript)
|
|
fmt.Cat(b.sscript == 0 ? a.sscript == 1 ? '`' : ',' :
|
|
b.sscript == 1 ? '`' : ',');
|
|
if(a.GetFace() != b.GetFace())
|
|
switch(b.GetFace()) {
|
|
case Font::ARIAL: fmt.Cat('A'); break;
|
|
case Font::ROMAN: fmt.Cat('R'); break;
|
|
case Font::COURIER: fmt.Cat('C'); break;
|
|
case Font::STDFONT: fmt.Cat('G'); break;
|
|
#ifdef PLATFORM_WIN32
|
|
case Font::SYMBOL: fmt.Cat('S'); break;
|
|
#endif
|
|
default:
|
|
fmt << "!" << b.GetFaceName() << "!";
|
|
}
|
|
if(a.link != b.link)
|
|
fmt << '^' << DeQtf(b.link) << '^';
|
|
if(a.indexentry != b.indexentry)
|
|
fmt << 'I' << DeQtf(ToUtf8(b.indexentry)) << ';';
|
|
if(a.ink != b.ink)
|
|
fmt << "@" << QtfFormat(b.ink);
|
|
if(a.paper != b.paper)
|
|
fmt << "$" << QtfFormat(b.paper);
|
|
if(a.GetHeight() != b.GetHeight()) {
|
|
for(int i = 0; i < 10; i++)
|
|
if(b.GetHeight() == QTFFontHeight[i]) {
|
|
SeparateNumber(fmt);
|
|
fmt.Cat('0' + i);
|
|
return;
|
|
}
|
|
fmt.Cat(Format("+%d", b.GetHeight()));
|
|
}
|
|
}
|
|
|
|
void FmtNumber(String& qtf, char c, int former, int current)
|
|
{
|
|
qtf << (former != current ? Format("%c%d;", c, current) : String());
|
|
}
|
|
|
|
void QTFEncodeParaFormat(String& qtf, const RichPara::Format& format, const RichPara::Format& style)
|
|
{
|
|
if(format.align != style.align)
|
|
switch(format.align) {
|
|
case ALIGN_LEFT: qtf << '<'; break;
|
|
case ALIGN_RIGHT: qtf << '>'; break;
|
|
case ALIGN_CENTER: qtf << '='; break;
|
|
case ALIGN_JUSTIFY: qtf << '#'; break;
|
|
}
|
|
FmtNumber(qtf, 'l', style.lm, format.lm);
|
|
FmtNumber(qtf, 'r', style.rm, format.rm);
|
|
FmtNumber(qtf, 'i', style.indent, format.indent);
|
|
FmtNumber(qtf, 'H', style.ruler, format.ruler);
|
|
if(style.rulerink != format.rulerink)
|
|
qtf << "h" << QtfFormat(format.rulerink);
|
|
FmtNumber(qtf, 'L', style.rulerstyle, format.rulerstyle);
|
|
FmtNumber(qtf, 'b', style.before, format.before);
|
|
FmtNumber(qtf, 'a', style.after, format.after);
|
|
if(style.newpage != format.newpage)
|
|
qtf << 'P';
|
|
if(style.keep != format.keep)
|
|
qtf << 'k';
|
|
if(style.keepnext != format.keepnext)
|
|
qtf << 'K';
|
|
if(style.orphan != format.orphan)
|
|
qtf << 'Q';
|
|
if(style.linespacing != format.linespacing)
|
|
switch(format.linespacing) {
|
|
case RichPara::LSP15: qtf << "ph"; break;
|
|
case RichPara::LSP20: qtf << "pd"; break;
|
|
default: qtf << "po"; break;
|
|
}
|
|
if(style.bullet != format.bullet) {
|
|
qtf << 'O';
|
|
switch(format.bullet) {
|
|
case RichPara::BULLET_NONE: qtf << '_'; break;
|
|
case RichPara::BULLET_ROUNDWHITE: qtf << '1'; break;
|
|
case RichPara::BULLET_BOX: qtf << '2'; break;
|
|
case RichPara::BULLET_BOXWHITE: qtf << '3'; break;
|
|
case RichPara::BULLET_TEXT: qtf << '9'; break;
|
|
default: qtf << '0'; break;
|
|
}
|
|
qtf << ';';
|
|
}
|
|
if(!IsEmpty(format.label))
|
|
qtf << ':' << DeQtf(format.label) << ':';
|
|
if(style.newhdrftr != format.newhdrftr)
|
|
qtf << "tP" << format.header_qtf << "^^" << format.footer_qtf << "^^";
|
|
|
|
if(NumberingDiffers(style, format)) {
|
|
if(format.before_number != style.before_number) {
|
|
qtf << "n";
|
|
qtf << DeQtf(format.before_number);
|
|
qtf << ';';
|
|
}
|
|
if(format.after_number != style.after_number) {
|
|
qtf << "m";
|
|
qtf << DeQtf(format.after_number);
|
|
qtf << ";";
|
|
}
|
|
int l;
|
|
for(l = 7; l >= 0; --l)
|
|
if(format.number[l])
|
|
break;
|
|
qtf << "N";
|
|
for(int i = 0; i <= l; i++) {
|
|
static char h[] = { '-', '1', '0', 'a', 'A', 'i', 'I' };
|
|
if(format.number[i] <= RichPara::NUMBER_I)
|
|
qtf << h[format.number[i]];
|
|
else
|
|
qtf << '-';
|
|
}
|
|
if(format.reset_number)
|
|
qtf << '!';
|
|
else
|
|
qtf << ';';
|
|
}
|
|
FmtNumber(qtf, 't', style.tabsize, format.tabsize);
|
|
if(style.tab != format.tab) {
|
|
qtf << "~~";
|
|
int i;
|
|
for(i = 0; i < format.tab.GetCount(); i++) {
|
|
RichPara::Tab tab = format.tab[i];
|
|
qtf << '~';
|
|
if(tab.align == ALIGN_RIGHT)
|
|
qtf << '>';
|
|
if(tab.align == ALIGN_CENTER)
|
|
qtf << '=';
|
|
if(tab.fillchar == 1)
|
|
qtf << '.';
|
|
if(tab.fillchar == 2)
|
|
qtf << '-';
|
|
if(tab.fillchar == 3)
|
|
qtf << '_';
|
|
qtf << tab.pos;
|
|
}
|
|
if(i)
|
|
qtf << ';';
|
|
}
|
|
}
|
|
|
|
bool s_nodeqtf[128];
|
|
|
|
void QTFEncodePara(String& qtf, const RichPara& p, const RichPara::Format& style, byte charset, dword lang, bool crlf)
|
|
{
|
|
int d = qtf.GetLength();
|
|
QTFEncodeParaFormat(qtf, p.format, style);
|
|
if(p.part.GetCount()) {
|
|
dword l = p.part.Top().format.language;
|
|
LngFmt(qtf, l, lang);
|
|
lang = l;
|
|
}
|
|
else {
|
|
CharFmt(qtf, style, p.format);
|
|
LngFmt(qtf, p.format.language, lang);
|
|
}
|
|
qtf.Cat(' ');
|
|
d = qtf.GetLength() - d;
|
|
for(int i = 0; i < p.part.GetCount(); i++) {
|
|
const RichPara::Part& part = p.part[i];
|
|
String cf;
|
|
LngFmt(cf, part.format.language, lang);
|
|
CharFmt(cf, style, part.format);
|
|
if(!cf.IsEmpty()) {
|
|
qtf << '[' << cf << ' ';
|
|
d += cf.GetLength();
|
|
}
|
|
if(part.field) {
|
|
if(crlf)
|
|
qtf << "\r\n";
|
|
qtf << "{:" + DeQtf(part.field.ToString()) + ":" + DeQtf(part.fieldparam) + ":}";
|
|
if(crlf)
|
|
qtf << "\r\n";
|
|
d = 0;
|
|
}
|
|
else
|
|
if(part.object) {
|
|
const RichObject& object = part.object;
|
|
Size sz = object.GetSize();
|
|
if(crlf)
|
|
qtf << "\r\n";
|
|
qtf << "@@" << object.GetTypeName() << ':' << sz.cx
|
|
<< (object.IsKeepRatio() ? '&' : '*') << sz.cy;
|
|
if(object.GetYDelta())
|
|
qtf << '/' << object.GetYDelta();
|
|
String data = object.Write();
|
|
const char *q = data.Begin();
|
|
const char *slim = data.End();
|
|
qtf.Reserve(8 * data.GetLength() / 7);
|
|
if(object.IsText()) {
|
|
qtf << "`";
|
|
while(q < slim) {
|
|
ASSERT((byte)*q >= 32);
|
|
if(*q == '`')
|
|
data.Cat('`');
|
|
qtf.Cat(*q++);
|
|
}
|
|
qtf << "`";
|
|
}
|
|
else {
|
|
if(crlf)
|
|
qtf << "\r\n";
|
|
#if 1
|
|
qtf << '(' << Base64Encode(q, slim) << ')';
|
|
#else
|
|
while(q < slim - 7) {
|
|
byte data[8];
|
|
data[0] = ((q[0] & 0x80) >> 7) |
|
|
((q[1] & 0x80) >> 6) |
|
|
((q[2] & 0x80) >> 5) |
|
|
((q[3] & 0x80) >> 4) |
|
|
((q[4] & 0x80) >> 3) |
|
|
((q[5] & 0x80) >> 2) |
|
|
((q[6] & 0x80) >> 1) |
|
|
0x80;
|
|
data[1] = q[0] | 0x80;
|
|
data[2] = q[1] | 0x80;
|
|
data[3] = q[2] | 0x80;
|
|
data[4] = q[3] | 0x80;
|
|
data[5] = q[4] | 0x80;
|
|
data[6] = q[5] | 0x80;
|
|
data[7] = q[6] | 0x80;
|
|
qtf.Cat(data, 8);
|
|
if(crlf && ++n % 10 == 0)
|
|
qtf << "\r\n";
|
|
q += 7;
|
|
}
|
|
while(q < slim) {
|
|
byte seven = 0;
|
|
const char *lim = slim;
|
|
const char *s;
|
|
for(s = q; s < lim; s++)
|
|
seven = (seven >> 1) | (*s & 0x80);
|
|
seven >>= 8 - (lim - q);
|
|
qtf.Cat(seven | 0x80);
|
|
for(s = q; s < lim; s++)
|
|
qtf.Cat(*s | 0x80);
|
|
if(crlf && ++n % 10 == 0)
|
|
qtf << "\r\n";
|
|
q += 7;
|
|
}
|
|
#endif
|
|
}
|
|
if(crlf)
|
|
qtf << "\r\n";
|
|
d = 0;
|
|
}
|
|
else {
|
|
for(const wchar *s = part.text.Begin(); s != part.text.End(); s++) {
|
|
int c = *s;
|
|
if(c < 128) {
|
|
if(s_nodeqtf[c]) {
|
|
qtf.Cat(c);
|
|
d++;
|
|
}
|
|
else
|
|
if(c == 9) {
|
|
qtf.Cat("-|");
|
|
d++;
|
|
}
|
|
else
|
|
if(c == ':' && s[1] != ':')
|
|
qtf.Cat(':');
|
|
else {
|
|
qtf.Cat('`');
|
|
qtf.Cat(c);
|
|
d += 2;
|
|
}
|
|
if(crlf && d > 60 && c == ' ') {
|
|
qtf.Cat("\r\n");
|
|
d = 0;
|
|
}
|
|
}
|
|
else {
|
|
if(c == 160) {
|
|
qtf.Cat("_");
|
|
d++;
|
|
}
|
|
else
|
|
if(charset == CHARSET_UTF8) {
|
|
String q = ToUtf8(c);
|
|
d += q.GetLength();
|
|
qtf << q;
|
|
}
|
|
else {
|
|
int ch = FromUnicode(c, charset, 0);
|
|
if(ch)
|
|
qtf << (char)ch;
|
|
else
|
|
qtf << "@$" << Format("%04X", ch) << ';';
|
|
d++;
|
|
}
|
|
}
|
|
if(crlf && d > 80) {
|
|
qtf.Cat("\r\n");
|
|
d = 0;
|
|
}
|
|
}
|
|
}
|
|
if(!cf.IsEmpty()) {
|
|
d++;
|
|
qtf << ']';
|
|
}
|
|
}
|
|
}
|
|
|
|
void FmtNumber2(String& qtf, char c, int da, int a, int db, int b)
|
|
{
|
|
if(da != a || db != b) {
|
|
qtf << c;
|
|
if(da != a)
|
|
qtf << a;
|
|
qtf << '/';
|
|
qtf << b;
|
|
}
|
|
}
|
|
|
|
|
|
void QTFEncodeTxt(String& qtf, const RichTxt& text, const RichStyles& styles, const RichStyle& defstyle,
|
|
dword options, const Index<Uuid>& sm, byte charset, dword lang)
|
|
{
|
|
qtf << '[';
|
|
for(int i = 0; i < text.GetPartCount(); i++) {
|
|
if(i) {
|
|
qtf << "&]";
|
|
if(options & QTF_CRLF)
|
|
qtf << "\r\n";
|
|
qtf << '[';
|
|
}
|
|
if(text.IsTable(i)) {
|
|
qtf << ' ';
|
|
|
|
const RichTable& t = text.GetTable(i);
|
|
const RichTable::Format& f = t.GetFormat();
|
|
int nx = f.column.GetCount();
|
|
int ny = t.GetRows();
|
|
qtf << "{{";
|
|
for(int i = 0; i < nx; i++) {
|
|
if(i)
|
|
qtf << ':';
|
|
qtf << f.column[i];
|
|
}
|
|
const RichTable::Format& d = Single<RichTable::Format>();
|
|
FmtNumber(qtf, '<', d.lm, f.lm);
|
|
FmtNumber(qtf, '>', d.rm, f.rm);
|
|
FmtNumber(qtf, 'B', d.before, f.before);
|
|
FmtNumber(qtf, 'A', d.after, f.after);
|
|
FmtNumber(qtf, 'f', d.frame, f.frame);
|
|
if(f.keep)
|
|
qtf << 'K';
|
|
if(f.newpage)
|
|
qtf << 'P';
|
|
if(f.newhdrftr)
|
|
qtf << 'T' << f.header_qtf << "^^" << f.footer_qtf << "^^";
|
|
if(f.framecolor != d.framecolor)
|
|
qtf << 'F' << QtfFormat(f.framecolor);
|
|
FmtNumber(qtf, 'g', d.grid, f.grid);
|
|
if(f.gridcolor != d.gridcolor)
|
|
qtf << 'G' << QtfFormat(f.gridcolor);
|
|
FmtNumber(qtf, 'h', d.header, f.header);
|
|
RichCell::Format cf = Single<RichCell::Format>();
|
|
for(int i = 0; i < ny; i++) {
|
|
const Array<RichCell>& r = t[i];
|
|
for(int j = 0; j < r.GetCount(); j++) {
|
|
const RichCell& c = r[j];
|
|
if(i || j) {
|
|
if(options & QTF_CRLF)
|
|
qtf << "\r\n";
|
|
qtf << "::";
|
|
}
|
|
const RichCell::Format& f = c.format;
|
|
if(f.align != cf.align)
|
|
switch(f.align) {
|
|
case ALIGN_TOP: qtf << '^'; break;
|
|
case ALIGN_CENTER: qtf << '='; break;
|
|
case ALIGN_BOTTOM: qtf << 'v'; break;
|
|
}
|
|
FmtNumber2(qtf, 'l', cf.border.left, f.border.left, cf.margin.left, f.margin.left);
|
|
FmtNumber2(qtf, 'r', cf.border.right, f.border.right, cf.margin.right, f.margin.right);
|
|
FmtNumber2(qtf, 't', cf.border.top, f.border.top, cf.margin.top, f.margin.top);
|
|
FmtNumber2(qtf, 'b', cf.border.bottom, f.border.bottom, cf.margin.bottom, f.margin.bottom);
|
|
FmtNumber(qtf, 'H', cf.minheight, f.minheight);
|
|
if(f.color != cf.color)
|
|
qtf << '@' << QtfFormat(f.color);
|
|
if(f.bordercolor != cf.bordercolor)
|
|
qtf << 'R' << QtfFormat(f.bordercolor);
|
|
cf = f;
|
|
if(c.hspan)
|
|
qtf << '-' << c.hspan;
|
|
if(c.vspan)
|
|
qtf << '|' << c.vspan;
|
|
if(f.keep)
|
|
qtf << "k";
|
|
if(f.round)
|
|
qtf << "o";
|
|
qtf << ' ';
|
|
QTFEncodeTxt(qtf, c.text, styles, defstyle, options, sm, charset, lang);
|
|
}
|
|
}
|
|
qtf << "}}";
|
|
}
|
|
else {
|
|
RichPara p = text.Get(i, styles);
|
|
int si = sm.Find(text.GetParaStyle(i));
|
|
if(!(options & QTF_NOSTYLES))
|
|
qtf << "s" << si;
|
|
SeparateNumber(qtf);
|
|
const RichStyle& s = si < 0 ? defstyle : GetStyle(styles, sm[si]);
|
|
QTFEncodePara(qtf, p, options & QTF_NOSTYLES ? defstyle.format : s.format, charset, lang,
|
|
(options & QTF_CRLF));
|
|
}
|
|
}
|
|
qtf << ']';
|
|
}
|
|
|
|
void init_s_nodeqtf()
|
|
{
|
|
ONCELOCK {
|
|
for(int c = 1; c < 128; c++)
|
|
s_nodeqtf[c] = IsAlNum(c) || strchr(".,;!?%()/<># ", c);
|
|
}
|
|
}
|
|
|
|
String AsQTF(const RichText& text, byte charset, dword options)
|
|
{
|
|
int i;
|
|
String qtf;
|
|
|
|
init_s_nodeqtf();
|
|
|
|
RichPara::Format dpf;
|
|
dpf.Face(Font::ARIAL);
|
|
dpf.Height(100);
|
|
dpf.language = LNG_ENGLISH;
|
|
|
|
bool crlf = options & QTF_CRLF;
|
|
|
|
Index<Uuid> sm;
|
|
// if(options & QTF_ALL_STYLES) //!!!! problem -> GetUsedStyles!!!!
|
|
for(i = 0; i < text.GetStyleCount(); i++)
|
|
sm.FindAdd(text.GetStyleId(i));
|
|
// else
|
|
// for(i = 0; i < text.GetPartCount(); i++)
|
|
// sm.FindAdd(text.GetParaStyle(i));
|
|
|
|
String hdr = text.GetHeaderQtf();
|
|
if(hdr.GetCount())
|
|
qtf << "^H" << hdr << "^^\r\n";
|
|
|
|
String ftr = text.GetFooterQtf();
|
|
if(ftr.GetCount())
|
|
qtf << "^F" << ftr << "^^\r\n";
|
|
|
|
if(!(options & QTF_NOSTYLES))
|
|
for(i = 0; i < sm.GetCount(); i++) {
|
|
Uuid id = sm[i];
|
|
const RichStyle& s = text.GetStyle(id);
|
|
qtf << '[';
|
|
QTFEncodeParaFormat(qtf, s.format, dpf);
|
|
CharFmt(qtf, dpf, s.format);
|
|
qtf << ' ';
|
|
qtf << "$$" << i << ',' << max(sm.Find(s.next), 0)
|
|
<< '#' << Format(id)
|
|
<< ':' << DeQtf(s.name) << "]";
|
|
if(crlf)
|
|
qtf << "\r\n";
|
|
}
|
|
|
|
if(text.GetPartCount() && (options & QTF_BODY)) {
|
|
VectorMap<dword, int> lngc;
|
|
int m = min(text.GetPartCount(), 30);
|
|
for(int i = 0; i < m; i++)
|
|
if(text.IsPara(i)) {
|
|
RichPara p = text.Get(i);
|
|
lngc.GetAdd(p.format.language, 0)++;
|
|
}
|
|
dword lang = lngc.GetCount() ? lngc.GetKey(FindMax(lngc.GetValues())) : 0;
|
|
qtf << "[";
|
|
if(!(options & QTF_NOCHARSET)) {
|
|
qtf << "{";
|
|
if(charset == CHARSET_UTF8)
|
|
qtf << "_";
|
|
else
|
|
if(charset >= CHARSET_WIN1250 && charset <= CHARSET_WIN1258)
|
|
qtf << (char)('0' + charset - CHARSET_WIN1250);
|
|
else
|
|
if(charset >= CHARSET_ISO8859_1 && charset <= CHARSET_ISO8859_16)
|
|
qtf << (char)('A' + charset - CHARSET_ISO8859_1);
|
|
else
|
|
qtf << CharsetName(charset);
|
|
qtf << "}";
|
|
}
|
|
if(lang && !(options & QTF_NOLANG))
|
|
qtf << "%" << LNGAsText(SetLNGCharset(lang, CHARSET_DEFAULT));
|
|
qtf << " ";
|
|
if(crlf)
|
|
qtf << "\r\n";
|
|
RichStyle defstyle;
|
|
defstyle.format.Height(100);
|
|
|
|
QTFEncodeTxt(qtf, text, text.GetStyles(), defstyle, options, sm, charset, lang);
|
|
|
|
qtf << "]";
|
|
}
|
|
return qtf;
|
|
}
|
|
|
|
String DeQtf(const char *s) {
|
|
StringBuffer r;
|
|
for(; *s; s++) {
|
|
if(*s == '\n')
|
|
r.Cat('&');
|
|
else {
|
|
if((byte)*s > ' ' && !IsDigit(*s) && !IsAlpha(*s) && (byte)*s < 128)
|
|
r.Cat('`');
|
|
r.Cat(*s);
|
|
}
|
|
}
|
|
return r;
|
|
}
|
|
|
|
String DeQtfLf(const char *s) {
|
|
StringBuffer r;
|
|
while(*s) {
|
|
if((byte)*s > ' ' && !IsDigit(*s) && !IsAlpha(*s) && (byte)*s < 128)
|
|
r.Cat('`');
|
|
if((byte)*s >= ' ')
|
|
r.Cat(*s);
|
|
else
|
|
if(*s == '\n')
|
|
r.Cat('&');
|
|
else
|
|
if(*s == '\t')
|
|
r.Cat("-|");
|
|
s++;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
RichText AsRichText(const RichObject& obj)
|
|
{
|
|
RichText x;
|
|
RichPara p;
|
|
RichPara::Format fmt;
|
|
p.Cat(obj, fmt);
|
|
x.Cat(p);
|
|
return x;
|
|
}
|
|
|
|
|
|
String AsQTF(const RichObject& obj)
|
|
{
|
|
return AsQTF(AsRichText(obj), CHARSET_UTF8, QTF_NOSTYLES|QTF_BODY);
|
|
}
|
|
|
|
}
|