Merge branch 'main' into Aristo3

This commit is contained in:
daboe01 authored and GitHub committed 2026-10-25 16:11:36 +01:00
commit 05bc99690d
15 files changed
+1300 -29

No files matched your search

+11 -3
View File
@@ -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;
}
+8 -4
View File
@@ -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]];
}
+10
View File
@@ -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;
}
+353 -4
View File
@@ -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];
}
+253 -3
View File
@@ -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