/* RTFParser.j Parse a RTF string into a CPAttributedString Copyright (C) 2014 Daniel Boehringer * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @import @import @import "CPFontManager.j" @import "CPParagraphStyle.j" @import "_CPTableTextAttachment.j" @global CPLeftTextAlignment @global CPRightTextAlignment @global CPCenterTextAlignment @global CPJustifiedTextAlignment @global CPNaturalTextAlignment @global CPFontAttributeName @global CPForegroundColorAttributeName @global CPBackgroundColorAttributeName @global CPParagraphStyleAttributeName @global CPAttachmentAttributeName @global CPUnderlineStyleAttributeName @global CPLeftTabStopType @global CPRightTabStopType @global CPCenterTabStopType @global CPDecimalTabStopType var hexTable = []; var cp1252Map = { 0x80: 0x20AC, 0x82: 0x201A, 0x83: 0x0192, 0x84: 0x201E, 0x85: 0x2026, 0x86: 0x2020, 0x87: 0x2021, 0x88: 0x02C6, 0x89: 0x2030, 0x8A: 0x0160, 0x8B: 0x2039, 0x8C: 0x0152, 0x8E: 0x017D, 0x91: 0x2018, 0x92: 0x2019, 0x93: 0x201C, 0x94: 0x201D, 0x95: 0x2022, 0x96: 0x2013, 0x97: 0x2014, 0x98: 0x02DC, 0x99: 0x2122, 0x9A: 0x0161, 0x9B: 0x203A, 0x9C: 0x0153, 0x9E: 0x017E, 0x9F: 0x0178 }; // Hold the attributes of the current run @implementation _RTFAttribute : CPObject { CPRange _range; CPParagraphStyle paragraph; CPColor fgColour; CPColor bgColour; CPColor ulColour; CPString fontName; unsigned fontSize; BOOL bold; BOOL italic; BOOL underline; BOOL strikethrough; BOOL script; BOOL _tabChanged; CPTabStopType _nextTabType; } - (id)init { if (self = [super init]) { [self resetFont]; [self resetParagraphStyle]; _range = CPMakeRange(0, 0); _nextTabType = CPLeftTabStopType; } return self; } - (id)copy { var mynew = [_RTFAttribute new]; mynew.paragraph = [paragraph mutableCopy]; mynew.fontName = fontName; mynew.fontSize = fontSize; mynew.bold = bold; mynew.italic = italic; mynew.underline = underline; mynew.strikethrough = strikethrough; mynew.script = script; mynew.fgColour = fgColour; mynew.bgColour = bgColour; mynew.ulColour = ulColour; mynew._tabChanged = _tabChanged; mynew._nextTabType = _nextTabType; return mynew; } - (CPFont)currentFont { var font = [CPFont _fontWithName:fontName size:fontSize bold:bold italic:italic]; if (font) return font; //Before giving up and using a default font, we try if this is //not the case of a font with a composite name, such as //'Helvetica-Light'. In that case, even if we don't have //exactly an 'Helvetica-Light' font family, we might have an //'Helvetica' one. var range = [fontName rangeOfString:@"-"]; if (range.location != CPNotFound) { var fontFamily = [fontName substringToIndex: range.location]; font = [CPFont fontWithName:fontFamily size:fontSize]; } /* Last resort, default font. :-( */ if (font == nil) font = [CPFont systemFontOfSize:fontSize]; return font; } - (CPNumber)script { return [CPNumber numberWithInt:script]; } - (CPNumber)underline { if (underline != 0) return [CPNumber numberWithInt:underline]; else return nil; } - (CPNumber)strikethrough { if (strikethrough != 0) return [CPNumber numberWithInt:strikethrough]; else return nil; } - (void)resetParagraphStyle { paragraph = [[CPParagraphStyle defaultParagraphStyle] mutableCopy]; _tabChanged = NO; _nextTabType = CPLeftTabStopType; } - (void)resetFont { var font = [CPFont systemFontOfSize:12]; fontName = [font familyName]; fontSize = 12.0; italic = NO; bold = NO; underline = 0; strikethrough = 0; script = 0; } - (void)addTab:(float)location type:(CPTextTabType)type { var tab = [[CPTextTab alloc] initWithType:type location:location]; if (!_tabChanged) { [paragraph setTabStops:[tab]]; _tabChanged = YES; } else { [paragraph addTabStop: tab]; } _nextTabType = CPLeftTabStopType; } - (CPDictionary)dictionary { var ret = @{}; [ret setObject:[self currentFont] forKey:CPFontAttributeName]; [ret setObject:paragraph forKey:CPParagraphStyleAttributeName]; if (fgColour) [ret setObject:fgColour forKey:CPForegroundColorAttributeName]; if (bgColour) [ret setObject:bgColour forKey:CPBackgroundColorAttributeName]; if (underline) [ret setObject:[CPNumber numberWithInt:1] forKey:CPUnderlineStyleAttributeName]; return ret; } @end // based on https://github.com/lazygyu/RTF-parser var kRTFParserType_char = 0, kRTFParserType_dest = 1, kRTFParserType_prop = 2, kRTFParserType_spec = 3; // Keyword descriptions var kRgsymRtf = { // keyword dflt fPassDflt kwd idx "b" : [ "b", 1, false, kRTFParserType_prop, "propBold"], "ul" : [ "ul", 1, false, kRTFParserType_prop, "propUnderline"], "i" : [ "i", 1, false, kRTFParserType_prop, "propItalic"], // "li" : [ "li", 0, false, kRTFParserType_prop, "propPgnFormat"], "pgnucltr" : [ "pgnucltr", "pgULtr", true, kRTFParserType_prop, "propPgnFormat"], "pgnlcltr" : [ "pgnlcltr", "pgLLtr", true, kRTFParserType_prop, "propPgnFormat"], "qc" : [ "qc", "justC", true, kRTFParserType_prop, "propJust"], "ql" : [ "ql", "justL", true, kRTFParserType_prop, "propJust"], "qr" : [ "qr", "justR", true, kRTFParserType_prop, "propJust"], "qj" : [ "qj", "justF", true, kRTFParserType_prop, "propJust"], "paperw" : [ "paperw", 12240, false, kRTFParserType_prop, "propXaPage"], "paperh" : [ "paperh", 15480, false, kRTFParserType_prop, "propYaPage"], "margl" : [ "margl", 1800, false, kRTFParserType_prop, "propXaLeft"], "margr" : [ "margr", 1800, false, kRTFParserType_prop, "propXaRight"], "margt" : [ "margt", 1440, false, kRTFParserType_prop, "propYaTop"], "margb" : [ "margb", 1440, false, kRTFParserType_prop, "propYaBottom"], "pgnstart" : [ "pgnstart", 1, true, kRTFParserType_prop, "propPgnStart"], "facingp" : [ "facingp", 1, true, kRTFParserType_prop, "propFacingp"], "landscape" : [ "landscape",1, true, kRTFParserType_prop, "propLandscape"], "par" : [ "par", 0, false, kRTFParserType_char, "\n"], "pard" : [ "pard", 0, false, kRTFParserType_prop, "propDefaultPara"], "\0x0a" : [ "\0x0a", 0, false, kRTFParserType_char, "\n"], "\0x0d" : [ "\0x0d", 0, false, kRTFParserType_char, ""], "tab" : [ "tab", 0, false, kRTFParserType_char, "\t"], "ldblquote" : [ "ldblquote",0, false, kRTFParserType_char, '"'], "rdblquote" : [ "rdblquote",0, false, kRTFParserType_char, '"'], "bin" : [ "bin", 0, false, kRTFParserType_spec, "ipfnBin"], "*" : [ "*", 0, false, kRTFParserType_spec, "ipfnDestSkip"], "'" : [ "'", 0, false, kRTFParserType_spec, "ipfnHex"], "author" : [ "author", 0, false, kRTFParserType_dest, "destSkip"], "buptim" : [ "buptim", 0, false, kRTFParserType_dest, "destSkip"], "colortbl" : [ "colortbl", 0, false, kRTFParserType_dest, "destSkip"], "comment" : [ "comment", 0, false, kRTFParserType_dest, "destSkip"], "creatim" : [ "creatim", 0, false, kRTFParserType_dest, "destSkip"], "doccomm" : [ "doccomm", 0, false, kRTFParserType_dest, "destSkip"], "fonttbl" : [ "fonttbl", 0, false, kRTFParserType_dest, "destSkip"], "footer" : [ "footer", 0, false, kRTFParserType_dest, "destSkip"], "footerf" : [ "footerf", 0, false, kRTFParserType_dest, "destSkip"], "footerl" : [ "footerl", 0, false, kRTFParserType_dest, "destSkip"], "footerr" : [ "footerr", 0, false, kRTFParserType_dest, "destSkip"], "footnote" : [ "footnote", 0, false, kRTFParserType_dest, "destSkip"], "ftncn" : [ "ftncn", 0, false, kRTFParserType_dest, "destSkip"], "ftnsep" : [ "ftnsep", 0, false, kRTFParserType_dest, "destSkip"], "ftnsepc" : [ "ftnsepc", 0, false, kRTFParserType_dest, "destSkip"], "fprq" : [ "fprq", 0, false, kRTFParserType_dest, "destSkip"], // "fcharset" : [ "fcharset", 0, false, kRTFParserType_dest, "destSkip"], "rquote" : [ "rquote", 0, false, kRTFParserType_char, "'"], // "s" : [ "s", 0, false, kRTFParserType_dest, "destSkip"], "header" : [ "header", 0, false, kRTFParserType_dest, "destSkip"], "headerf" : [ "headerf", 0, false, kRTFParserType_dest, "destSkip"], "headerl" : [ "headerl", 0, false, kRTFParserType_dest, "destSkip"], "headerr" : [ "headerr", 0, false, kRTFParserType_dest, "destSkip"], "info" : [ "info", 0, false, kRTFParserType_dest, "destSkip"], "keywords" : [ "keywords", 0, false, kRTFParserType_dest, "destSkip"], "operator" : [ "operator", 0, false, kRTFParserType_dest, "destSkip"], "pict" : [ "pict", 0, false, kRTFParserType_dest, "destSkip"], "printim" : [ "printim", 0, false, kRTFParserType_dest, "destSkip"], "private1" : [ "private1", 0, false, kRTFParserType_dest, "destSkip"], "revtim" : [ "revtim", 0, false, kRTFParserType_dest, "destSkip"], "rxe" : [ "rxe", 0, false, kRTFParserType_dest, "destSkip"], "stylesheet" : [ "stylesheet",0, false, kRTFParserType_dest, "destSkip"], "subject" : [ "subject", 0, false, kRTFParserType_dest, "destSkip"], "tc" : [ "tc", 0, false, kRTFParserType_dest, "destSkip"], "title" : [ "title", 0, false, kRTFParserType_dest, "destSkip"], "txe" : [ "txe", 0, false, kRTFParserType_dest, "destSkip"], "xe" : [ "xe", 0, false, kRTFParserType_dest, "destSkip"], "[" : [ "[", 0, false, kRTFParserType_char, '['], " " : [ " ", 0, false, kRTFParserType_char, ' '], "]" : [ "]", 0, false, kRTFParserType_char, ']'], "{" : [ "{", 0, false, kRTFParserType_char, '{'], "}" : [ "}", 0, false, kRTFParserType_char, '}'], "\\" : [ "\\", 0, false, kRTFParserType_char, '\\'], "trowd" : [ "trowd", 0, false, kRTFParserType_spec, "ipfnTrowd"], "cell" : [ "cell", 0, false, kRTFParserType_spec, "ipfnCell"], "row" : [ "row", 0, false, kRTFParserType_spec, "ipfnRow"], "cellx" : [ "cellx", 0, false, kRTFParserType_spec, "ipfnCellx"], "intbl" : [ "intbl", 0, false, kRTFParserType_spec, "ipfnIntbl"] }; @implementation _CPRTFParser : CPObject { CPString _codePage; CGSize _paper; CPString _rtf; unsigned _curState; CPArray _states; unsigned _currentParseIndex; BOOL _hexreturn; _RTFAttribute _currentRun; CPAttributedString _result; CPArray _colorArray; CPArray _fontArray; CPString _freename; BOOL _parsingFontTable; // Table parsing state BOOL _inTableActive; BOOL _waitingForNextRow; CPMutableArray _tableRows; CPMutableArray _currentRow; CPString _currentCellText; } - (id)init { if (self = [super init]) { _paper = CPMakeSize(0, 0); _rtf = ""; _curState = 0; // 0 = normal, 1 = skip _states = []; _currentParseIndex = 0; _hexreturn = NO; _result = [CPAttributedString new]; _colorArray = []; _fontArray = ['Arial']; // FIXME: should be name of system font _freename = ""; _parsingFontTable = NO; _inTableActive = NO; _waitingForNextRow = NO; _tableRows = nil; _currentRow = nil; _currentCellText = ""; // Safe Initialization _currentRun = [_RTFAttribute new]; } return self; } - (CPString)_checkChar:(CPArray)sym parameter:(CPString)ch { switch (_curState) { case 0: if (sym && sym[4]) return sym[4]; case 1: // CPLogConsole("skipped : " + sym[4]); return ''; default: if (sym && sym[4]) return sym[4]; } } - (BOOL)pushState { // Push stack as an object containing scoping context _states.push({ curState: _curState, run: [_currentRun copy] }); return YES; } - (BOOL)popState { if (_states.length > 0) { var state = _states.pop(); _curState = state.curState; [self _flushCurrentRun]; _currentRun = state.run; // Safety guard to prevent setting properties on null if (!_currentRun) { _currentRun = [_RTFAttribute new]; } _currentRun._range = CPMakeRange([_result length], 0); if (_curState == 0) { _parsingFontTable = NO; } } return YES; } - (CPString)_parseSpec:(CPArray)sym parameter:(CPString)v { var ch = ''; switch (sym[4]) { case "ipfnDestSkip": _curState++; return ''; case "ipfnHex": var hex = ''; // Konsumiere exakt 2 Zeichen nach dem \' for (var i = 0; i < 2; i++) { var nextCh = _rtf.charAt(++_currentParseIndex); if (/[a-fA-F0-9]/.test(nextCh)) { hex += nextCh; } else { _currentParseIndex--; break; } } _hexreturn = YES; if (_curState !== 0) return ''; else return hex; break; case "codePage": ch = _rtf.charAt(++_currentParseIndex); var code = ''; while (new RegExp("[0-9]").test(ch)) { code += (ch + ''); ch = _rtf.charAt(++_currentParseIndex); } _codePage = code; _currentParseIndex--; break; case "ipfnTrowd": if (_waitingForNextRow) { _waitingForNextRow = NO; } if (!_inTableActive) { _inTableActive = YES; _tableRows = [CPMutableArray array]; _currentRow = [CPMutableArray array]; _currentCellText = ""; } else { _currentRow = [CPMutableArray array]; } return ''; case "ipfnIntbl": return ''; case "ipfnCell": if (_inTableActive) { if (!_currentRow) _currentRow = [CPMutableArray array]; [_currentRow addObject:_currentCellText]; _currentCellText = ""; } return ''; case "ipfnRow": if (_inTableActive) { if (!_currentRow) _currentRow = [CPMutableArray array]; [_tableRows addObject:_currentRow]; _waitingForNextRow = YES; } return ''; case "ipfnCellx": return ''; } return ''; } - (void)_flushTableIfAny { if (_tableRows && [_tableRows count] > 0) { var headers = [_tableRows objectAtIndex:0]; var rows = [CPMutableArray array]; for (var idx = 1; idx < [_tableRows count]; idx++) { [rows addObject:[_tableRows objectAtIndex:idx]]; } var attachment = [[_CPTableTextAttachment alloc] initWithHeaders:headers rows:rows width:500.0]; // Linear height allocation estimation var numCols = [headers count]; var estimatedHeight = 36.0 + ([rows count] * 28.0); var lineCount = Math.ceil(estimatedHeight / 16.0) + 1; var newlineStr = ""; for (var nl = 0; nl < lineCount; nl++) { newlineStr += "\n"; } var tableAttrStr = [[CPMutableAttributedString alloc] initWithString:newlineStr]; [tableAttrStr addAttribute:@"TableAttachmentAttribute" value:attachment range:CPMakeRange(0, [tableAttrStr length])]; [tableAttrStr addAttribute:CPAttachmentAttributeName value:attachment range:CPMakeRange(0, [tableAttrStr length])]; [_result appendAttributedString:tableAttrStr]; _tableRows = nil; _currentRow = nil; _currentCellText = ""; _inTableActive = NO; _waitingForNextRow = NO; } } - (void)_flushCurrentRun { var newOffset = 0; if (_currentRun) { if ([_result length] == _currentRun._range.location) return; _currentRun._range.length = [_result length] - _currentRun._range.location; newOffset = CPMaxRange(_currentRun._range); var dict = [_currentRun dictionary]; [_result setAttributes:dict range:_currentRun._range]; // flush previous run // Deep copy the current run style for the next sequence of characters _currentRun = [_currentRun copy]; } else { _currentRun = [_RTFAttribute new]; } _currentRun._range = CPMakeRange(newOffset, 0); // open a new one } - (CPString)_applyPropChange:sym parameter:param { //console.log("prop : " + sym[0] + " / param : " + param+ ' '); switch (sym[0]) { case "pard": [self _flushCurrentRun]; [_currentRun resetParagraphStyle]; break; case "b": // bold if (param === 0) { if (_currentRun && _currentRun.bold) [self _flushCurrentRun]; _currentRun.bold = NO; } else { if (_currentRun && !_currentRun.bold) [self _flushCurrentRun]; _currentRun.bold = YES; } break; case "i": // italic if (param === 0) { if (_currentRun && _currentRun.italic) [self _flushCurrentRun]; _currentRun.italic = NO; } else { if (_currentRun && !_currentRun.italic) [self _flushCurrentRun]; _currentRun.italic = YES; } break; case "ul": // underline if (param === 0) { if (_currentRun && _currentRun.underline) [self _flushCurrentRun]; _currentRun.underline = NO; } else { if (_currentRun && !_currentRun.underline) [self _flushCurrentRun]; _currentRun.underline = YES; } break; case "qc": // paragraph center [_currentRun.paragraph setAlignment:CPCenterTextAlignment]; break; case "ql": // paragraph left [_currentRun.paragraph setAlignment:CPLeftTextAlignment]; break; case "qr": // paragraph right [_currentRun.paragraph setAlignment:CPRightTextAlignment]; break; case "qj": // paragraph justified [_currentRun.paragraph setAlignment:CPJustifiedTextAlignment]; break; case "paperw": _paper.width = param; break; case "paperh": _paper.height = param; break; } return ''; } - (CPString)_changeDest:(CPArray)sym { switch (sym[0]) { case "colortbl": _colorArray.push([CPColor blackColor]); break; case "fonttbl": _parsingFontTable = YES; break; } if (sym[4] == "destSkip") { CPLogConsole("Dest skip start : [" + sym[0] + "]"); _curState++; } return ''; } - (CPString)_translateKeyword:(CPString)keyword parameter:(CPString)param fParameter:(BOOL)fParam { if (_waitingForNextRow) { if (keyword !== "trowd" && keyword !== "cell" && keyword !== "row" && keyword !== "intbl" && keyword !== "cellx") { [self _flushTableIfAny]; } } if (kRgsymRtf[keyword] !== undefined) { var sym = kRgsymRtf[keyword]; switch (sym[3]) { case kRTFParserType_prop: if (sym[2] || !fParam) param = sym[1]; return [self _applyPropChange:sym parameter:param]; case kRTFParserType_char: if((param + '') !== 'NaN' && (param + '') !== 'null' && (param + '').length) _currentParseIndex -= (param + '').length; return [self _checkChar:sym parameter:param]; case kRTFParserType_dest: return [self _changeDest:sym]; case kRTFParserType_spec: return [self _parseSpec:sym parameter:param]; default: return ''; } } else { switch (keyword) { case "red": var oldColor = [_colorArray lastObject], green = [oldColor greenComponent], blue = [oldColor blueComponent]; _colorArray.pop(); _colorArray.push([CPColor colorWithRed:parseInt(param) / 255 green:green blue:blue alpha:1.0]); break; case "green": var oldColor = [_colorArray lastObject], red = [oldColor redComponent], blue = [oldColor blueComponent]; _colorArray.pop(); _colorArray.push([CPColor colorWithRed:red green: parseInt(param) / 255 blue:blue alpha:1.0]); break; case "blue": var oldColor = [_colorArray lastObject], green = [oldColor greenComponent], red = [oldColor redComponent]; _colorArray.pop(); _colorArray.push([CPColor colorWithRed:red green:green blue:parseInt(param) / 255 alpha:1.0]); _colorArray.push([CPColor blackColor]); // placeholder for next color break; case "cf": // change foreground color [self _flushCurrentRun]; var fontIndex = parseInt(param) - 1; if (_currentRun && fontIndex >= 0) _currentRun.fgColour = _colorArray[fontIndex]; break; case "cb": // change background color case "highlight": [self _flushCurrentRun]; var colorIndex = parseInt(param) - 1; if (_currentRun) { if (colorIndex >= 0 && colorIndex < _colorArray.length) _currentRun.bgColour = _colorArray[colorIndex]; else _currentRun.bgColour = nil; } break; case "f": // change font [self _flushCurrentRun]; var fontIndex = parseInt(param); if (_currentRun && fontIndex >= 0 && fontIndex < _fontArray.length) _currentRun.fontName = _fontArray[fontIndex]; break; case "fs": // change font size [self _flushCurrentRun]; _currentRun.fontSize = parseInt(param) / 2; break; case "fi": // first line indent if (_currentRun) [_currentRun.paragraph setFirstLineHeadIndent:parseInt(param) / 20.0]; break; case "li": // left indent / head indent if (_currentRun) [_currentRun.paragraph setHeadIndent:parseInt(param) / 20.0]; break; case "ri": // right indent / tail indent if (_currentRun) [_currentRun.paragraph setTailIndent:parseInt(param) / 20.0]; break; case "tqc": // center tab stop style flag if (_currentRun) _currentRun._nextTabType = CPCenterTabStopType; break; case "tqr": // right tab stop style flag if (_currentRun) _currentRun._nextTabType = CPRightTabStopType; break; case "tqdec": // decimal tab stop style flag if (_currentRun) _currentRun._nextTabType = CPDecimalTabStopType; break; case "tx": // tabstop location definition var location = parseInt(param) / 20; if (_currentRun) [_currentRun addTab:location type:_currentRun._nextTabType]; break; default: CPLogConsole("skip : " + keyword + " param: " + param); } return ''; } } - (CPString)_parseKeyword:(CPString)rtf length:(unsigned)len { var ch = '', fParam = false, fNeg = false, keyword = '', param = ''; _rtf = rtf; if (++_currentParseIndex >= len) return len; ch = rtf.charAt(_currentParseIndex); if (!/[a-zA-Z]/.test(ch)) return [self _translateKeyword:ch parameter:nil fParameter:fParam]; while (new RegExp("[a-zA-Z]").test(ch)) { keyword += ch; ch = rtf.charAt(++_currentParseIndex); } if (ch == '-') { fNeg = true; ch = rtf.charAt(++_currentParseIndex); } fParam = true; while (new RegExp("[0-9]").test(ch)) { param += (ch + ''); ch = rtf.charAt(++_currentParseIndex); } _currentParseIndex--; param = parseInt(param); if (fNeg) param *= -1; return [self _translateKeyword:keyword parameter:param fParameter:fParam]; } - (void)_appendPlainString:(CPString) aString { if (_inTableActive) { _currentCellText += aString; } else { [_result replaceCharactersInRange:CPMakeRange([_result length], 0) withString:aString]; } } - (CPAttributedString)parseRTF:(CPString)rtf { rtf = rtf.replace(/\\\n/g, "\\par\n"); if (rtf.length == 0) return ''; _currentParseIndex = -1; var len = rtf.length, tmp = '', ch = '', hex = '', lastchar = 0; while (_currentParseIndex < len) { tmp = rtf.charAt(++_currentParseIndex); if (_waitingForNextRow && tmp !== "\\" && tmp !== " " && tmp !== "\n" && tmp !== "\r" && tmp !== "\t") { [self _flushTableIfAny]; } if (tmp !== "\\" && hex.length > 0) { [self _appendPlainString: String.fromCharCode(parseInt((hex), 16))]; hex = ''; } switch (tmp) { case " ": if (lastchar == 1) { lastchar = 0; } else { _freename += tmp; [self _appendPlainString:tmp]; } break; case "{": if (_waitingForNextRow) [self _flushTableIfAny]; if ([self pushState]) CPLogConsole("push"); break; case "}": if (_waitingForNextRow) [self _flushTableIfAny]; if ([self popState]) CPLogConsole("pop"); if (_freename) { CPLogConsole(_freename); if (_parsingFontTable) { _fontArray.push(_freename); _parsingFontTable = NO; } _freename = ""; } [self _flushCurrentRun]; break; case "\\": _freename = ''; ch = [self _parseKeyword:rtf length:len]; if (!_hexreturn && ch.length == 0) lastchar = 1; else lastchar = 0; if (_hexreturn) { if (ch.length > 0) { var byteVal = parseInt(ch, 16); var unicodeVal = byteVal; // Windows-1252 Mapping für den Bereich 0x80 - 0x9F anwenden if (byteVal >= 0x80 && byteVal <= 0x9F) { unicodeVal = cp1252Map[byteVal] || byteVal; } [self _appendPlainString: String.fromCharCode(unicodeVal)]; } _hexreturn = NO; } else if (ch !== undefined && _curState === 0) { [self _appendPlainString:ch]; } break; case 0x0d: case 0x0a: case '\n': case '\r': break; default: lastchar = 0; if (_curState == 0) { [self _appendPlainString:tmp]; } else { if (tmp === ';') { if (_parsingFontTable && _freename) { var cleanFontName = _freename.trim(); // strip family name prefix if present (e.g., "swiss Helvetica" -> "Helvetica") var lastSpaceIdx = cleanFontName.lastIndexOf(' '); if (lastSpaceIdx !== -1) { cleanFontName = cleanFontName.substring(lastSpaceIdx + 1); } _fontArray.push(cleanFontName); _freename = ""; } } else { _freename += tmp; } } break; } } [self _flushTableIfAny]; return _result; } @end