mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-31 04:17:36 +00:00
Merge branch 'main' into Aristo3
This commit is contained in:
15 files changed
+1300
-29
No files matched your search
@@ -1541,11 +1541,19 @@ var _objectsInRange = function(aList, aRange)
|
||||
|
||||
if (verticalOffset)
|
||||
{
|
||||
// An attachment view is moved through setFrameOrigin:, which also moves it on the
|
||||
// screen. Changing its frame directly would leave it in place, because the next draw's
|
||||
// setFrameOrigin: would then find it already at the new origin and do nothing.
|
||||
if (_runs[i].view)
|
||||
_runs[i].view._frame.origin.y += verticalOffset;
|
||||
{
|
||||
var viewOrigin = [_runs[i].view frameOrigin];
|
||||
|
||||
if (_runs[i].elem)
|
||||
_runs[i].elem.top = (_runs[i].elem.top + verticalOffset) + 'px';
|
||||
[_runs[i].view setFrameOrigin:CGPointMake(viewOrigin.x, viewOrigin.y + verticalOffset)];
|
||||
}
|
||||
|
||||
// A span that has been placed moves with its line. One that has not is placed when drawn.
|
||||
if (_runs[i].elem && _runs[i].elem.style.top)
|
||||
_runs[i].elem.style.top = (parseFloat(_runs[i].elem.style.top) + verticalOffset) + 'px';
|
||||
|
||||
_runs[i].DOMpatched = YES;
|
||||
}
|
||||
|
||||
@@ -5045,16 +5045,20 @@ var _CPCopyPlaceholder = '-';
|
||||
// Can we accept richtext? Then this is our preference (fixme: shift key to force plain text paste)
|
||||
if ([currentFirstResponder respondsToSelector:@selector(isRichText)] && [currentFirstResponder isRichText])
|
||||
{
|
||||
var richtext = nativeClipboard.getData('text/rtf');
|
||||
var richtext = nativeClipboard.getData('text/rtf'),
|
||||
richData = [pasteboard stringForType:_CPASPboardType];
|
||||
|
||||
// RTF that this application wrote itself, on copy, describes the text on the internal
|
||||
// pasteboard. The internal archive is pasted instead: it also keeps what RTF does not
|
||||
// yet carry, such as images.
|
||||
if (richtext && richData && richtext === [pasteboard stringForType:CPRTFPboardType])
|
||||
richtext = [CPKeyedUnarchiver unarchiveObjectWithData:[CPData dataWithRawString:richData]];
|
||||
// prefer RTF form the outside of cappuccino
|
||||
if (richtext)
|
||||
else if (richtext)
|
||||
richtext = [[_CPRTFParser new] parseRTF:richtext];
|
||||
else
|
||||
{
|
||||
// If no RTF is available, try to get the internal represatation of richtext from the pasteboard
|
||||
var richData = [pasteboard stringForType:_CPASPboardType];
|
||||
|
||||
if (richData)
|
||||
richtext = [CPKeyedUnarchiver unarchiveObjectWithData:[CPData dataWithRawString:richData]];
|
||||
}
|
||||
|
||||
@@ -510,6 +510,16 @@ var _CPTypesetterDecimalSeparator = function()
|
||||
if (viewSize.height > _lineHeight)
|
||||
_lineHeight = viewSize.height - descent + leading;
|
||||
|
||||
// An attachment wider than the container stays on the line it overflows, so a break
|
||||
// there keeps the line's height and baseline with the attachment's size applied.
|
||||
// Otherwise the attachment would be placed on the baseline of the text before it,
|
||||
// and drawn above its line.
|
||||
if (CPMaxRange(wrapRange) === CPMaxRange(lineRange))
|
||||
{
|
||||
wrapRange._height = _lineHeight;
|
||||
wrapRange._base = _lineBase;
|
||||
}
|
||||
|
||||
ascent = viewSize.height;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
@import "CPFontManager.j"
|
||||
@import "CPParagraphStyle.j"
|
||||
@import "_CPTableTextAttachment.j"
|
||||
@import "CPTextList.j"
|
||||
|
||||
@global CPLeftTextAlignment
|
||||
@global CPRightTextAlignment
|
||||
@@ -57,6 +58,209 @@ var cp1252Map = {
|
||||
0x9E: 0x017E, 0x9F: 0x0178
|
||||
};
|
||||
|
||||
/*
|
||||
The marker format names of the RTF number formats (\levelnfc). 23 is a bullet.
|
||||
*/
|
||||
var _CPRTFNumberFormatNames = {
|
||||
0: "decimal",
|
||||
1: "upper-roman",
|
||||
2: "lower-roman",
|
||||
3: "upper-alpha",
|
||||
4: "lower-alpha"
|
||||
},
|
||||
_CPRTFBulletFormats = {
|
||||
"\u2022": "{disc}",
|
||||
"\u25E6": "{circle}",
|
||||
"\u25AA": "{square}",
|
||||
"\u2043": "{hyphen}",
|
||||
"\u25C6": "{diamond}",
|
||||
"\u2713": "{check}",
|
||||
"\u25A1": "{box}"
|
||||
};
|
||||
|
||||
/*
|
||||
Returns the index just after the group that opens with the brace at anOpenIndex in rtf. Escaped
|
||||
braces, \{ and \}, do not open or close groups.
|
||||
*/
|
||||
var _CPRTFGroupEnd = function(rtf, anOpenIndex)
|
||||
{
|
||||
var depth = 0;
|
||||
|
||||
for (var i = anOpenIndex; i < rtf.length; i++)
|
||||
{
|
||||
var character = rtf.charAt(i);
|
||||
|
||||
if (character === "\\")
|
||||
i++;
|
||||
else if (character === "{")
|
||||
depth++;
|
||||
else if (character === "}" && --depth === 0)
|
||||
return i + 1;
|
||||
}
|
||||
|
||||
return rtf.length;
|
||||
};
|
||||
|
||||
/*
|
||||
Returns the groups in rtf that open with aPattern, a regular expression that matches the
|
||||
opening brace and the group's first control word.
|
||||
*/
|
||||
var _CPRTFGroups = function(rtf, aPattern)
|
||||
{
|
||||
var groups = [],
|
||||
match;
|
||||
|
||||
aPattern.lastIndex = 0;
|
||||
|
||||
while ((match = aPattern.exec(rtf)))
|
||||
{
|
||||
var end = _CPRTFGroupEnd(rtf, match.index);
|
||||
|
||||
groups.push(rtf.substring(match.index, end));
|
||||
aPattern.lastIndex = end;
|
||||
}
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
/*
|
||||
Decodes the text of a list level, the content of its \leveltext destination after the length
|
||||
byte, into parts: strings, and numbers for the levels whose item numbers take their place. A
|
||||
\'hh escape with a value below 9 is a level number; any other is a character.
|
||||
*/
|
||||
var _CPRTFLevelTextParts = function(aLevelText)
|
||||
{
|
||||
var parts = [],
|
||||
literal = "",
|
||||
i = 0;
|
||||
|
||||
while (i < aLevelText.length)
|
||||
{
|
||||
var character = aLevelText.charAt(i);
|
||||
|
||||
if (character === ";")
|
||||
break;
|
||||
|
||||
if (character !== "\\")
|
||||
{
|
||||
literal += character;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var hexMatch = /^\\'([0-9a-fA-F]{2})/.exec(aLevelText.substring(i)),
|
||||
unicodeMatch = /^\\u(-?\d+) ?\??/.exec(aLevelText.substring(i)),
|
||||
wordMatch = /^\\[a-z]+-?\d* ?/.exec(aLevelText.substring(i));
|
||||
|
||||
if (hexMatch)
|
||||
{
|
||||
var value = parseInt(hexMatch[1], 16);
|
||||
|
||||
if (value < 9)
|
||||
{
|
||||
if (literal)
|
||||
parts.push(literal);
|
||||
|
||||
literal = "";
|
||||
parts.push(value);
|
||||
}
|
||||
else
|
||||
literal += String.fromCharCode(value);
|
||||
|
||||
i += hexMatch[0].length;
|
||||
}
|
||||
else if (unicodeMatch)
|
||||
{
|
||||
var code = parseInt(unicodeMatch[1], 10);
|
||||
|
||||
literal += String.fromCharCode(code < 0 ? code + 65536 : code);
|
||||
i += unicodeMatch[0].length;
|
||||
}
|
||||
else if (wordMatch)
|
||||
i += wordMatch[0].length;
|
||||
else
|
||||
{
|
||||
literal += aLevelText.charAt(i + 1);
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (literal)
|
||||
parts.push(literal);
|
||||
|
||||
return parts;
|
||||
};
|
||||
|
||||
/*
|
||||
Returns the definition of one list level, {format, options, start}, from its \listlevel group.
|
||||
Cocoa writes the marker format in \levelmarker. For other writers, the format is made from the
|
||||
number format and the level's text.
|
||||
*/
|
||||
var _CPRTFListLevelDefinition = function(aLevelGroup, aLevel)
|
||||
{
|
||||
var markerMatch = /\{\\\*\\levelmarker ?((?:\\.|[^\\}])*)\}/.exec(aLevelGroup),
|
||||
startMatch = /\\levelstartat(-?\d+)/.exec(aLevelGroup),
|
||||
numberFormatMatch = /\\levelnfc(\d+)/.exec(aLevelGroup),
|
||||
levelTextMatch = /\{\\leveltext(?:\\leveltemplateid-?\d+)? ?\\'[0-9a-fA-F]{2}((?:\\.|[^\\}])*)\}/.exec(aLevelGroup),
|
||||
parts = levelTextMatch ? _CPRTFLevelTextParts(levelTextMatch[1]) : [],
|
||||
prepends = /\\levelprepend(?![a-z])/.test(aLevelGroup),
|
||||
format = nil;
|
||||
|
||||
for (var i = 0; i < parts.length; i++)
|
||||
if (typeof parts[i] === "number" && parts[i] < aLevel)
|
||||
prepends = YES;
|
||||
|
||||
if (markerMatch)
|
||||
format = markerMatch[1].replace(/\\([\\{}])/g, "$1");
|
||||
else
|
||||
{
|
||||
var numberFormat = numberFormatMatch ? parseInt(numberFormatMatch[1], 10) : 0;
|
||||
|
||||
if (numberFormat === 23)
|
||||
{
|
||||
var bullet = parts.join("");
|
||||
|
||||
format = _CPRTFBulletFormats[bullet] || bullet || CPTextListMarkerDisc;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The text around this level's own number. Text after an enclosing level's number
|
||||
// ends that level's marker, which is prepended, so only text before any number is
|
||||
// this level's prefix. Its suffix runs up to the next number.
|
||||
var name = "{" + (_CPRTFNumberFormatNames[numberFormat] || "decimal") + "}",
|
||||
prefix = "",
|
||||
suffix = "",
|
||||
found = NO,
|
||||
seenEnclosingNumber = NO;
|
||||
|
||||
for (var i = 0; i < parts.length; i++)
|
||||
{
|
||||
if (parts[i] === aLevel)
|
||||
found = YES;
|
||||
else if (typeof parts[i] === "number")
|
||||
{
|
||||
if (found)
|
||||
break;
|
||||
|
||||
seenEnclosingNumber = YES;
|
||||
}
|
||||
else if (found)
|
||||
suffix += parts[i];
|
||||
else if (!seenEnclosingNumber)
|
||||
prefix += parts[i];
|
||||
}
|
||||
|
||||
format = prefix + name + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
format: format,
|
||||
options: prepends ? CPTextListPrependEnclosingMarker : 0,
|
||||
start: startMatch ? parseInt(startMatch[1], 10) : 1
|
||||
};
|
||||
};
|
||||
|
||||
// Hold the attributes of the current run
|
||||
@implementation _RTFAttribute : CPObject
|
||||
{
|
||||
@@ -76,6 +280,13 @@ var cp1252Map = {
|
||||
CPTabStopType _nextTabType;
|
||||
int superscript;
|
||||
float baselineOffset;
|
||||
|
||||
// The first line indent, \fi, is an offset from the left indent, \li, in RTF.
|
||||
float _firstLineOffset;
|
||||
|
||||
// The RTF list (\lsN) and level (\ilvlN) of a list item, or 0 and 0.
|
||||
int _listNumber;
|
||||
int _listLevel;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
@@ -110,6 +321,9 @@ var cp1252Map = {
|
||||
mynew._nextTabType = _nextTabType;
|
||||
mynew.superscript = superscript;
|
||||
mynew.baselineOffset = baselineOffset;
|
||||
mynew._firstLineOffset = _firstLineOffset;
|
||||
mynew._listNumber = _listNumber;
|
||||
mynew._listLevel = _listLevel;
|
||||
|
||||
return mynew;
|
||||
}
|
||||
@@ -162,6 +376,9 @@ var cp1252Map = {
|
||||
paragraph = [[CPParagraphStyle defaultParagraphStyle] mutableCopy];
|
||||
_tabChanged = NO;
|
||||
_nextTabType = CPLeftTabStopType;
|
||||
_firstLineOffset = 0.0;
|
||||
_listNumber = 0;
|
||||
_listLevel = 0;
|
||||
}
|
||||
|
||||
- (void)resetFont
|
||||
@@ -342,6 +559,17 @@ var kRgsymRtf = {
|
||||
CPMutableArray _tableRows;
|
||||
CPMutableArray _currentRow;
|
||||
CPString _currentCellText;
|
||||
|
||||
// Text lists: the level definitions of each RTF list by its \listid, the list of each
|
||||
// override by its \ls number, and the text lists made for each override, one for each level.
|
||||
Object _listDefinitions;
|
||||
Object _listOverrides;
|
||||
Object _textListsByOverride;
|
||||
|
||||
// Unicode: the number of fallback characters after each \uN (\ucN), and how many of them are
|
||||
// still to be skipped.
|
||||
int _unicodeFallbackCount;
|
||||
int _fallbackCharactersToSkip;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
@@ -367,6 +595,13 @@ var kRgsymRtf = {
|
||||
_currentRow = nil;
|
||||
_currentCellText = "";
|
||||
|
||||
_listDefinitions = {};
|
||||
_listOverrides = {};
|
||||
_textListsByOverride = {};
|
||||
|
||||
_unicodeFallbackCount = 1;
|
||||
_fallbackCharactersToSkip = 0;
|
||||
|
||||
// Safe Initialization
|
||||
_currentRun = [_RTFAttribute new];
|
||||
}
|
||||
@@ -834,16 +1069,51 @@ var kRgsymRtf = {
|
||||
_currentRun.fontSize = parseInt(param) / 2;
|
||||
break;
|
||||
|
||||
case "fi": // first line indent
|
||||
case "fi": // first line indent, an offset from the left indent
|
||||
if (_currentRun)
|
||||
[_currentRun.paragraph setFirstLineHeadIndent:parseInt(param) / 20.0];
|
||||
{
|
||||
_currentRun._firstLineOffset = parseInt(param) / 20.0;
|
||||
[_currentRun.paragraph setFirstLineHeadIndent:[_currentRun.paragraph headIndent] + _currentRun._firstLineOffset];
|
||||
}
|
||||
break;
|
||||
|
||||
case "li": // left indent / head indent
|
||||
if (_currentRun)
|
||||
{
|
||||
[_currentRun.paragraph setHeadIndent:parseInt(param) / 20.0];
|
||||
[_currentRun.paragraph setFirstLineHeadIndent:[_currentRun.paragraph headIndent] + _currentRun._firstLineOffset];
|
||||
}
|
||||
break;
|
||||
|
||||
case "ls": // the RTF list of a list item
|
||||
if (_currentRun)
|
||||
{
|
||||
_currentRun._listNumber = parseInt(param);
|
||||
[self _applyTextListsToCurrentRun];
|
||||
}
|
||||
break;
|
||||
|
||||
case "ilvl": // the level of a list item
|
||||
if (_currentRun)
|
||||
{
|
||||
_currentRun._listLevel = parseInt(param);
|
||||
[self _applyTextListsToCurrentRun];
|
||||
}
|
||||
break;
|
||||
|
||||
case "uc": // the number of fallback characters after each Unicode character
|
||||
_unicodeFallbackCount = parseInt(param) || 0;
|
||||
break;
|
||||
|
||||
case "u": // a Unicode character, followed by its fallback characters
|
||||
var code = parseInt(param);
|
||||
|
||||
// Fallback characters are skipped only in text that is kept.
|
||||
if (_curState === 0)
|
||||
_fallbackCharactersToSkip = _unicodeFallbackCount;
|
||||
|
||||
return String.fromCharCode(code < 0 ? code + 65536 : code);
|
||||
|
||||
case "ri": // right indent / tail indent
|
||||
if (_currentRun)
|
||||
[_currentRun.paragraph setTailIndent:parseInt(param) / 20.0];
|
||||
@@ -932,6 +1202,76 @@ var kRgsymRtf = {
|
||||
return [self _translateKeyword:keyword parameter:param fParameter:fParam];
|
||||
}
|
||||
|
||||
/*
|
||||
Reads the list table and the list override table, which are destinations the parser otherwise
|
||||
skips, before the text is parsed. Paragraphs then refer to the lists with \lsN and \ilvlN.
|
||||
*/
|
||||
- (void)_parseListTables:(CPString)rtf
|
||||
{
|
||||
var lists = _CPRTFGroups(rtf, /\{\\list(?![a-z])/g);
|
||||
|
||||
for (var i = 0; i < lists.length; i++)
|
||||
{
|
||||
// Only the list itself has a \listid; its levels have \leveltemplateid.
|
||||
var idMatch = /\\listid(-?\d+)/.exec(lists[i]),
|
||||
levels = _CPRTFGroups(lists[i], /\{\\listlevel(?![a-z])/g),
|
||||
definitions = [];
|
||||
|
||||
if (!idMatch)
|
||||
continue;
|
||||
|
||||
for (var level = 0; level < levels.length; level++)
|
||||
definitions.push(_CPRTFListLevelDefinition(levels[level], level));
|
||||
|
||||
_listDefinitions[idMatch[1]] = definitions;
|
||||
}
|
||||
|
||||
var overrides = _CPRTFGroups(rtf, /\{\\listoverride(?![a-z])/g);
|
||||
|
||||
for (var i = 0; i < overrides.length; i++)
|
||||
{
|
||||
var idMatch = /\\listid(-?\d+)/.exec(overrides[i]),
|
||||
numberMatch = /\\ls(\d+)/.exec(overrides[i]);
|
||||
|
||||
if (idMatch && numberMatch)
|
||||
_listOverrides[numberMatch[1]] = idMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Gives the current run's paragraph the text lists of its RTF list and level. The text lists of
|
||||
an RTF list are made once, one for each level, so the items of a list share them and are
|
||||
numbered together.
|
||||
*/
|
||||
- (void)_applyTextListsToCurrentRun
|
||||
{
|
||||
var listNumber = _currentRun._listNumber;
|
||||
|
||||
if (!listNumber)
|
||||
{
|
||||
[_currentRun.paragraph setTextLists:[]];
|
||||
return;
|
||||
}
|
||||
|
||||
var definitions = _listDefinitions[_listOverrides[listNumber]] || [],
|
||||
textLists = _textListsByOverride[listNumber];
|
||||
|
||||
if (!textLists)
|
||||
textLists = _textListsByOverride[listNumber] = [];
|
||||
|
||||
for (var level = 0; level <= _currentRun._listLevel; level++)
|
||||
{
|
||||
if (!textLists[level])
|
||||
{
|
||||
var definition = definitions[level] || {format: CPTextListMarkerDisc, options: 0, start: 1};
|
||||
|
||||
textLists[level] = [[CPTextList alloc] initWithMarkerFormat:definition.format options:definition.options startingItemNumber:definition.start];
|
||||
}
|
||||
}
|
||||
|
||||
[_currentRun.paragraph setTextLists:textLists.slice(0, _currentRun._listLevel + 1)];
|
||||
}
|
||||
|
||||
- (void)_appendPlainString:(CPString) aString
|
||||
{
|
||||
if (_inTableActive)
|
||||
@@ -953,6 +1293,8 @@ var kRgsymRtf = {
|
||||
|
||||
_currentParseIndex = -1;
|
||||
|
||||
[self _parseListTables:rtf];
|
||||
|
||||
var len = rtf.length,
|
||||
tmp = '',
|
||||
ch = '',
|
||||
@@ -1035,7 +1377,9 @@ var kRgsymRtf = {
|
||||
if (_hexreturn)
|
||||
{
|
||||
// Only append decoded characters if we are in the active body state
|
||||
if (ch.length > 0 && _curState === 0)
|
||||
if (ch.length > 0 && _curState === 0 && _fallbackCharactersToSkip > 0)
|
||||
_fallbackCharactersToSkip--;
|
||||
else if (ch.length > 0 && _curState === 0)
|
||||
{
|
||||
var byteVal = parseInt(ch, 16);
|
||||
var unicodeVal = byteVal;
|
||||
@@ -1065,7 +1409,12 @@ var kRgsymRtf = {
|
||||
default:
|
||||
lastchar = 0;
|
||||
|
||||
if (_curState == 0)
|
||||
if (_curState == 0 && _fallbackCharactersToSkip > 0)
|
||||
{
|
||||
// A fallback character for the Unicode character before it.
|
||||
_fallbackCharactersToSkip--;
|
||||
}
|
||||
else if (_curState == 0)
|
||||
{
|
||||
[self _appendPlainString:tmp];
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
@import "CPGraphics.j"
|
||||
@import "CPFontManager.j"
|
||||
@import "_CPTableTextAttachment.j"
|
||||
@import "CPTextList.j"
|
||||
|
||||
@global CPForegroundColorAttributeName
|
||||
@global CPBackgroundColorAttributeName
|
||||
@@ -57,6 +58,44 @@ var PAPERSIZE = @"PaperSize",
|
||||
|
||||
function _points2twips(a) { return (a) * 20.0; }
|
||||
|
||||
/*
|
||||
Escapes text for RTF. Backslashes and braces are escaped, and every character outside ASCII
|
||||
is written as a Unicode control word, \uN, with no fallback characters (\uc0), as Cocoa
|
||||
writes it. Values above 32767 are written as negative numbers, as RTF requires.
|
||||
*/
|
||||
var _CPRTFEscapeUnicode = function(aString)
|
||||
{
|
||||
return aString.replace(/[^\x00-\x7f]/g, function(aCharacter)
|
||||
{
|
||||
var code = aCharacter.charCodeAt(0);
|
||||
|
||||
return "\\uc0\\u" + (code > 32767 ? code - 65536 : code) + " ";
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Returns aNumber as two hexadecimal digits, for the \'hh escapes of a list level's text.
|
||||
*/
|
||||
var _CPRTFHexByte = function(aNumber)
|
||||
{
|
||||
return ("0" + (aNumber & 0xff).toString(16)).slice(-2);
|
||||
};
|
||||
|
||||
/*
|
||||
The RTF number format (\levelnfc) of each marker format name. Bullets are 23. Hexadecimal and
|
||||
octal have no RTF number format, so other readers show them as decimal numbers; Cocoa reads
|
||||
the marker format itself from \levelmarker.
|
||||
*/
|
||||
var _CPRTFLevelNumberFormats = {
|
||||
"decimal": 0,
|
||||
"upper-roman": 1,
|
||||
"lower-roman": 2,
|
||||
"upper-alpha": 3,
|
||||
"upper-latin": 3,
|
||||
"lower-alpha": 4,
|
||||
"lower-latin": 4
|
||||
};
|
||||
|
||||
@implementation _CPRTFProducer : CPObject
|
||||
{
|
||||
CPAttributedString text;
|
||||
@@ -68,6 +107,11 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
CPColor fgColor;
|
||||
CPColor bgColor;
|
||||
CPColor ulColor;
|
||||
|
||||
// The outermost text lists in the text, each written as one RTF list, and for each, the text
|
||||
// list of every level, as first met.
|
||||
CPArray textLists;
|
||||
CPArray textListLevels;
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
@@ -94,6 +138,9 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
|
||||
fgColor = [CPColor blackColor];
|
||||
bgColor= [CPColor whiteColor];
|
||||
|
||||
textLists = [];
|
||||
textListLevels = [];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -246,6 +293,7 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
result = [CPString stringWithString:@"{\\rtf1\\ansi"];
|
||||
result += [self fontTable];
|
||||
result += [self colorTable];
|
||||
result += [self listTables];
|
||||
result += [self documentAttributes];
|
||||
|
||||
return result;
|
||||
@@ -313,7 +361,8 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
break;
|
||||
}
|
||||
|
||||
var twips = _points2twips([paraStyle firstLineHeadIndent]);
|
||||
// In RTF the first line indent is an offset from the left indent.
|
||||
var twips = _points2twips([paraStyle firstLineHeadIndent] - [paraStyle headIndent]);
|
||||
|
||||
if (twips != 0.0)
|
||||
headerString += [CPString stringWithFormat:@"\\fi%d", twips];
|
||||
@@ -375,12 +424,165 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
headerString += [CPString stringWithFormat:@"\\tx%d",_points2twips([tab location])];
|
||||
}
|
||||
|
||||
// A list item names its RTF list and its level.
|
||||
var lists = [paraStyle textLists];
|
||||
|
||||
if ([lists count])
|
||||
headerString += [CPString stringWithFormat:@"\\ls%d\\ilvl%d", [self listNumberForTextLists:lists], [lists count] - 1];
|
||||
|
||||
return headerString;
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Text lists
|
||||
|
||||
/*
|
||||
Returns the number of the RTF list for a paragraph's text lists, and records the list of each
|
||||
level. Each outermost text list becomes one RTF list, with one level for each nesting depth.
|
||||
*/
|
||||
- (int)listNumberForTextLists:(CPArray)someLists
|
||||
{
|
||||
var index = [textLists indexOfObjectIdenticalTo:someLists[0]];
|
||||
|
||||
if (index === CPNotFound)
|
||||
{
|
||||
index = textLists.length;
|
||||
textLists.push(someLists[0]);
|
||||
textListLevels.push([]);
|
||||
}
|
||||
|
||||
var levels = textListLevels[index];
|
||||
|
||||
for (var level = 0; level < someLists.length; level++)
|
||||
if (!levels[level])
|
||||
levels[level] = someLists[level];
|
||||
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
/*
|
||||
Returns the list table and the list override table for the text lists in the text. Each RTF
|
||||
list is a hybrid list with one level for each nesting depth, and one override, \lsN, that
|
||||
paragraphs refer to.
|
||||
*/
|
||||
- (CPString)listTables
|
||||
{
|
||||
if (!textLists.length)
|
||||
return @"";
|
||||
|
||||
var listTable = @"{\\*\\listtable",
|
||||
overrideTable = @"{\\*\\listoverridetable";
|
||||
|
||||
for (var i = 0; i < textLists.length; i++)
|
||||
{
|
||||
var listNumber = i + 1,
|
||||
levels = textListLevels[i];
|
||||
|
||||
listTable += "{\\list\\listtemplateid" + listNumber + "\\listhybrid";
|
||||
|
||||
for (var level = 0; level < levels.length; level++)
|
||||
listTable += [self listLevelString:levels atLevel:level templateID:listNumber * 100 + level + 1];
|
||||
|
||||
listTable += "{\\listname ;}\\listid" + listNumber + "}";
|
||||
overrideTable += "{\\listoverride\\listid" + listNumber + "\\listoverridecount0\\ls" + listNumber + "}";
|
||||
}
|
||||
|
||||
return listTable + "}\n" + overrideTable + "}\n";
|
||||
}
|
||||
|
||||
/*
|
||||
Returns the RTF definition of one list level. Cocoa reads the marker format from the
|
||||
\levelmarker destination and the prepending of the enclosing marker from \levelprepend. Other
|
||||
readers use the number format (\levelnfc), the starting number (\levelstartat) and the
|
||||
level's text (\leveltext), in which \'0N stands for the number of level N.
|
||||
*/
|
||||
- (CPString)listLevelString:(CPArray)someLevels atLevel:(int)aLevel templateID:(int)aTemplateID
|
||||
{
|
||||
var list = someLevels[aLevel],
|
||||
format = [list markerFormat],
|
||||
match = format.match(/\{([a-z-]+)\}/),
|
||||
name = match ? match[1] : "",
|
||||
numberFormat = [list isOrdered] ? (_CPRTFLevelNumberFormats[name] || 0) : 23,
|
||||
parts = [self levelTextParts:someLevels atLevel:aLevel],
|
||||
levelText = "",
|
||||
levelNumbers = "",
|
||||
length = 0,
|
||||
indent = 720 * (aLevel + 1);
|
||||
|
||||
for (var i = 0; i < parts.length; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
|
||||
if (typeof part === "number")
|
||||
{
|
||||
length++;
|
||||
levelText += "\\'" + _CPRTFHexByte(part);
|
||||
levelNumbers += "\\'" + _CPRTFHexByte(length);
|
||||
}
|
||||
else
|
||||
{
|
||||
length += part.length;
|
||||
levelText += _CPRTFEscapeUnicode(part.replace(/[\\{};]/g, function(aCharacter)
|
||||
{
|
||||
return "\\'" + _CPRTFHexByte(aCharacter.charCodeAt(0));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return "{\\listlevel\\levelnfc" + numberFormat + "\\levelnfcn" + numberFormat +
|
||||
"\\leveljc0\\leveljcn0\\levelfollow0\\levelstartat" + [list startingItemNumber] +
|
||||
"\\levelspace360\\levelindent0{\\*\\levelmarker " + _CPRTFEscapeUnicode(format.replace(/([\\{}])/g, "\\$1")) + "}" +
|
||||
(([list listOptions] & CPTextListPrependEnclosingMarker) ? "\\levelprepend" : "") +
|
||||
"{\\leveltext\\leveltemplateid" + aTemplateID + "\\'" + _CPRTFHexByte(length) + levelText + ";}" +
|
||||
"{\\levelnumbers" + levelNumbers + ";}\\fi-720\\li" + indent + "\\lin" + indent + " }";
|
||||
}
|
||||
|
||||
/*
|
||||
Returns the parts of a list level's text: strings, and the level numbers whose item numbers
|
||||
take their place. A bullet level is its bullet. A numbered level is the text around its number,
|
||||
after the enclosing level's text when the list prepends the enclosing marker.
|
||||
*/
|
||||
- (CPArray)levelTextParts:(CPArray)someLevels atLevel:(int)aLevel
|
||||
{
|
||||
var list = someLevels[aLevel],
|
||||
format = [list markerFormat];
|
||||
|
||||
if (![list isOrdered])
|
||||
return [[list markerForItemNumber:1]];
|
||||
|
||||
var match = format.match(/\{[a-z-]+\}/),
|
||||
parts = [];
|
||||
|
||||
if (aLevel > 0 && ([list listOptions] & CPTextListPrependEnclosingMarker))
|
||||
parts = [self levelTextParts:someLevels atLevel:aLevel - 1];
|
||||
|
||||
if (match.index > 0)
|
||||
parts.push(format.substring(0, match.index));
|
||||
|
||||
parts.push(aLevel);
|
||||
|
||||
if (match.index + match[0].length < format.length)
|
||||
parts.push(format.substring(match.index + match[0].length));
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
- (CPString)runStringForString:(CPString) substring
|
||||
attributes:(CPDictionary) attributes
|
||||
paragraphStart:(BOOL) first
|
||||
{
|
||||
return [self runStringForString:substring attributes:attributes paragraphStart:first listMarker:NO];
|
||||
}
|
||||
|
||||
/*
|
||||
As -runStringForString:attributes:paragraphStart:. When isListMarker is YES, the run is the
|
||||
marker at the start of a list item, and is written in a \listtext group, as Cocoa writes it.
|
||||
Readers that understand lists number the items themselves; others show the marker text.
|
||||
*/
|
||||
- (CPString)runStringForString:(CPString) substring
|
||||
attributes:(CPDictionary) attributes
|
||||
paragraphStart:(BOOL) first
|
||||
listMarker:(BOOL) isListMarker
|
||||
{
|
||||
var unwrap = function(obj) {
|
||||
if (!obj) return null;
|
||||
@@ -704,11 +906,18 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
}
|
||||
}
|
||||
|
||||
// An image attachment is not written yet, so its attachment character is left out.
|
||||
substring = substring.replace(/\uFFFC/g, '');
|
||||
|
||||
substring = substring.replace(/\\/g, '\\\\');
|
||||
substring = substring.replace(/\n/g, '\\par\n');
|
||||
substring = substring.replace(/\t/g, '\\tab ');
|
||||
substring = substring.replace(/{/g, '\\{');
|
||||
substring = substring.replace(/}/g, '\\}');
|
||||
substring = _CPRTFEscapeUnicode(substring);
|
||||
|
||||
if (isListMarker)
|
||||
substring = "{\\listtext " + substring + "}";
|
||||
|
||||
if (!first)
|
||||
{
|
||||
@@ -752,13 +961,29 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
substring,
|
||||
runString;
|
||||
|
||||
attributes = [text attributesAtIndex:CPMaxRange(currRange)
|
||||
var runStart = CPMaxRange(currRange);
|
||||
|
||||
attributes = [text attributesAtIndex:runStart
|
||||
longestEffectiveRange:currRange
|
||||
inRange:completeRange];
|
||||
|
||||
// The longest effective range can reach back before the run's start, after a list marker
|
||||
// that shares its attributes was written as a run of its own.
|
||||
if (currRange.location < runStart)
|
||||
currRange = CPMakeRange(runStart, CPMaxRange(currRange) - runStart);
|
||||
|
||||
// A list item's marker, a tab, the marker and a tab, is written as a run of its own.
|
||||
var markerLength = paragraphStart ? [self listMarkerLengthAtIndex:currRange.location attributes:attributes] : 0,
|
||||
isListMarker = markerLength > 0 && markerLength <= currRange.length;
|
||||
|
||||
if (isListMarker)
|
||||
currRange.length = markerLength;
|
||||
|
||||
substring = [string substringWithRange:currRange];
|
||||
runString = [self runStringForString:substring
|
||||
attributes:attributes
|
||||
paragraphStart:paragraphStart];
|
||||
paragraphStart:paragraphStart
|
||||
listMarker:isListMarker];
|
||||
|
||||
result += runString;
|
||||
|
||||
@@ -772,6 +997,31 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Returns the length of the marker at the start of a list item, a tab, the marker and a tab, or
|
||||
0 when the paragraph is not a list item or does not start with a marker.
|
||||
*/
|
||||
- (unsigned)listMarkerLengthAtIndex:(unsigned)anIndex attributes:(CPDictionary)attributes
|
||||
{
|
||||
var string = [text string];
|
||||
|
||||
if (![[[attributes objectForKey:CPParagraphStyleAttributeName] textLists] count] || string.charAt(anIndex) !== "\t")
|
||||
return 0;
|
||||
|
||||
for (var i = anIndex + 1; i < string.length; i++)
|
||||
{
|
||||
var character = string.charAt(i);
|
||||
|
||||
if (character === "\t")
|
||||
return i - anIndex + 1;
|
||||
|
||||
if (character === "\n")
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (CPString)RTFDStringFromAttributedString:(CPAttributedString)aText
|
||||
documentAttributes:(CPDictionary)dict
|
||||
{
|
||||
|
||||
@@ -556,3 +556,39 @@
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var _CPTableTextAttachmentHeadersKey = @"_CPTableTextAttachmentHeaders",
|
||||
_CPTableTextAttachmentRowsKey = @"_CPTableTextAttachmentRows",
|
||||
_CPTableTextAttachmentWidthKey = @"_CPTableTextAttachmentWidth",
|
||||
_CPTableTextAttachmentIsEditableKey = @"_CPTableTextAttachmentIsEditable",
|
||||
_CPTableTextAttachmentAcceptsRichTextKey = @"_CPTableTextAttachmentAcceptsRichText";
|
||||
|
||||
@implementation _CPTableTextAttachment (CPCoding)
|
||||
|
||||
/*
|
||||
Decodes the table from its headers, rows, width and settings, and builds its cells again.
|
||||
The cells are not decoded as subviews; they are made from the decoded text.
|
||||
*/
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
return [self initWithHeaders:[aCoder decodeObjectForKey:_CPTableTextAttachmentHeadersKey]
|
||||
rows:[aCoder decodeObjectForKey:_CPTableTextAttachmentRowsKey]
|
||||
width:[aCoder decodeFloatForKey:_CPTableTextAttachmentWidthKey] || 500.0
|
||||
isEditable:[aCoder decodeBoolForKey:_CPTableTextAttachmentIsEditableKey]
|
||||
acceptsRichText:[aCoder decodeBoolForKey:_CPTableTextAttachmentAcceptsRichTextKey]];
|
||||
}
|
||||
|
||||
/*
|
||||
Encodes the table's current text, including edits made in its cells, its width and its
|
||||
settings. The cell views are not encoded.
|
||||
*/
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeObject:[self headers] forKey:_CPTableTextAttachmentHeadersKey];
|
||||
[aCoder encodeObject:[self rows] forKey:_CPTableTextAttachmentRowsKey];
|
||||
[aCoder encodeFloat:CGRectGetWidth([self frame]) forKey:_CPTableTextAttachmentWidthKey];
|
||||
[aCoder encodeBool:_isEditable forKey:_CPTableTextAttachmentIsEditableKey];
|
||||
[aCoder encodeBool:_acceptsRichText forKey:_CPTableTextAttachmentAcceptsRichTextKey];
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in new issue
Block a user