mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-25 17:45:36 +00:00
Merge pull request #3347 from enquora/integration-test-document-based-app
Merge TextEditor fixes: lists and Unicode in RTF, images in pasted text * AppKit: The RTF producer and parser carry text lists, in Cocoa's form and readable from Word's, Unicode characters, and the first line indent relative to the left indent. * AppKit: Copy and paste within an application keeps images, tables and lists, through the internal archive. Tables can now be archived. * TextEditor: The work list records that browsers deliver only plain text to native applications, so rich text for them needs HTML.
This commit is contained in:
7 files changed
+804
-14
No files matched your search
@@ -5040,16 +5040,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]];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,138 @@
|
||||
@import <AppKit/CPApplication.j>
|
||||
@import <AppKit/CPTextView.j>
|
||||
|
||||
@implementation CPRTFTest : OJTestCase
|
||||
{
|
||||
CPWindow theWindow;
|
||||
CPTextView textView;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 1024.0, 768.0) styleMask:CPWindowNotSizable];
|
||||
textView = [[CPTextView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)];
|
||||
|
||||
[textView setRichText:YES];
|
||||
[[theWindow contentView] addSubview:textView];
|
||||
[theWindow makeFirstResponder:textView];
|
||||
}
|
||||
|
||||
- (CPAttributedString)roundTrip:(CPAttributedString)aString
|
||||
{
|
||||
var rtf = [_CPRTFProducer produceRTF:aString documentAttributes:@{}];
|
||||
|
||||
return [[_CPRTFParser new] parseRTF:rtf];
|
||||
}
|
||||
|
||||
- (CPArray)textListsAtIndex:(unsigned)anIndex ofString:(CPAttributedString)aString
|
||||
{
|
||||
return [[aString attribute:CPParagraphStyleAttributeName atIndex:anIndex effectiveRange:nil] textLists];
|
||||
}
|
||||
|
||||
// MARK: Unicode
|
||||
|
||||
- (void)testUnicodeIsEscapedAndRead
|
||||
{
|
||||
var text = @"Café — “quoted” •",
|
||||
rtf = [_CPRTFProducer produceRTF:[[CPAttributedString alloc] initWithString:text] documentAttributes:@{}];
|
||||
|
||||
[self assertTrue:rtf.indexOf("\\u8212 ") !== -1 message:@"an em dash is written as \\u8212"];
|
||||
[self assertTrue:/^[\x00-\x7f]*$/.test(rtf) message:@"the RTF holds only ASCII"];
|
||||
[self assert:text equals:[[[_CPRTFParser new] parseRTF:rtf] string]];
|
||||
}
|
||||
|
||||
- (void)testUnicodeFallbackCharactersAreSkipped
|
||||
{
|
||||
var parsed = [[_CPRTFParser new] parseRTF:@"{\\rtf1\\ansi\\uc1 a\\u8212?b \\uc0\\u8211 c}"];
|
||||
|
||||
[self assert:@"a—b –c" equals:[parsed string]];
|
||||
}
|
||||
|
||||
// MARK: Indents
|
||||
|
||||
- (void)testFirstLineIndentIsRelativeToTheLeftIndent
|
||||
{
|
||||
var parsed = [[_CPRTFParser new] parseRTF:@"{\\rtf1\\ansi\\pard\\li720\\fi-720 text}"],
|
||||
paragraphStyle = [parsed attribute:CPParagraphStyleAttributeName atIndex:0 effectiveRange:nil];
|
||||
|
||||
[self assert:36.0 equals:[paragraphStyle headIndent]];
|
||||
[self assert:0.0 equals:[paragraphStyle firstLineHeadIndent]];
|
||||
|
||||
var style = [[CPParagraphStyle defaultParagraphStyle] mutableCopy];
|
||||
|
||||
[style setHeadIndent:36.0];
|
||||
[style setFirstLineHeadIndent:18.0];
|
||||
|
||||
var roundTripped = [self roundTrip:[[CPAttributedString alloc] initWithString:@"text" attributes:@{CPParagraphStyleAttributeName: style}]],
|
||||
roundTrippedStyle = [roundTripped attribute:CPParagraphStyleAttributeName atIndex:0 effectiveRange:nil];
|
||||
|
||||
[self assert:36.0 equals:[roundTrippedStyle headIndent]];
|
||||
[self assert:18.0 equals:[roundTrippedStyle firstLineHeadIndent]];
|
||||
}
|
||||
|
||||
// MARK: Lists
|
||||
|
||||
- (void)testListFromTextEdit
|
||||
{
|
||||
var rtf = @"{\\rtf1\\ansi\\ansicpg1252\\cocoartf2822\n" +
|
||||
@"{\\fonttbl\\f0\\fswiss\\fcharset0 Helvetica;}\n" +
|
||||
@"{\\colortbl;\\red255\\green255\\blue255;}\n" +
|
||||
@"{\\*\\expandedcolortbl;;}\n" +
|
||||
@"{\\*\\listtable{\\list\\listtemplateid1\\listhybrid{\\listlevel\\levelnfc23\\levelnfcn23\\leveljc0\\leveljcn0\\levelfollow0\\levelstartat1\\levelspace360\\levelindent0{\\*\\levelmarker \\{disc\\}}{\\leveltext\\leveltemplateid1\\'01\\uc0\\u8226 ;}{\\levelnumbers;}\\fi-360\\li720\\lin720 }{\\listname ;}\\listid1}}\n" +
|
||||
@"{\\*\\listoverridetable{\\listoverride\\listid1\\listoverridecount0\\ls1}}\n" +
|
||||
@"\\pard\\tx220\\tx720\\li720\\fi-720\\pardirnatural\\partightenfactor0\n" +
|
||||
@"\\ls1\\ilvl0\n" +
|
||||
@"\\f0\\fs24 \\cf0 {\\listtext\t\\uc0\\u8226 \t}one\\\n" +
|
||||
@"{\\listtext\t\\uc0\\u8226 \t}two}",
|
||||
parsed = [[_CPRTFParser new] parseRTF:rtf],
|
||||
firstLists = [self textListsAtIndex:0 ofString:parsed],
|
||||
secondLists = [self textListsAtIndex:[[parsed string] length] - 1 ofString:parsed];
|
||||
|
||||
[self assert:@"\t•\tone\n\t•\ttwo" equals:[parsed string]];
|
||||
[self assert:1 equals:[firstLists count]];
|
||||
[self assert:@"{disc}" equals:[firstLists[0] markerFormat]];
|
||||
[self assert:firstLists[0] equals:secondLists[0] message:@"the items of one RTF list share one text list"];
|
||||
[self assert:0.0 equals:[[parsed attribute:CPParagraphStyleAttributeName atIndex:0 effectiveRange:nil] firstLineHeadIndent]];
|
||||
}
|
||||
|
||||
- (void)testListFromLevelTextWithoutMarkerFormat
|
||||
{
|
||||
var rtf = @"{\\rtf1\\ansi{\\*\\listtable{\\list\\listtemplateid1{\\listlevel\\levelnfc4\\levelstartat3{\\leveltext\\'03(\\'00);}{\\levelnumbers\\'02;}}{\\listlevel\\levelnfc0{\\leveltext\\'03\\'00.\\'01;}{\\levelnumbers\\'01\\'03;}}\\listid7}}" +
|
||||
@"{\\*\\listoverridetable{\\listoverride\\listid7\\listoverridecount0\\ls2}}" +
|
||||
@"\\pard\\ls2\\ilvl1 item}",
|
||||
lists = [self textListsAtIndex:0 ofString:[[_CPRTFParser new] parseRTF:rtf]];
|
||||
|
||||
[self assert:2 equals:[lists count]];
|
||||
[self assert:@"({lower-alpha})" equals:[lists[0] markerFormat]];
|
||||
[self assert:3 equals:[lists[0] startingItemNumber]];
|
||||
[self assert:@"{decimal}" equals:[lists[1] markerFormat]];
|
||||
[self assertTrue:([lists[1] listOptions] & CPTextListPrependEnclosingMarker) message:@"a level text with the enclosing number prepends it"];
|
||||
}
|
||||
|
||||
- (void)testNestedListRoundTrip
|
||||
{
|
||||
[textView setString:@"one\ntwo\nthree"];
|
||||
[textView setSelectedRange:CPMakeRange(0, 13)];
|
||||
[textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:CPTextListPrependEnclosingMarker]];
|
||||
[textView setSelectedRange:CPMakeRange([[textView string] rangeOfString:@"two"].location, 0)];
|
||||
[textView insertTab:nil];
|
||||
|
||||
var original = [textView textStorage],
|
||||
rtf = [_CPRTFProducer produceRTF:original documentAttributes:@{}],
|
||||
parsed = [[_CPRTFParser new] parseRTF:rtf],
|
||||
nestedIndex = [[parsed string] rangeOfString:@"two"].location,
|
||||
nestedLists = [self textListsAtIndex:nestedIndex ofString:parsed];
|
||||
|
||||
[self assertTrue:rtf.indexOf("{\\listtext ") !== -1 message:@"markers are written in \\listtext groups"];
|
||||
[self assertTrue:rtf.indexOf("\\levelprepend") !== -1];
|
||||
[self assert:[original string] equals:[parsed string]];
|
||||
[self assert:2 equals:[nestedLists count]];
|
||||
[self assert:@"{decimal}." equals:[nestedLists[1] markerFormat]];
|
||||
[self assertTrue:([nestedLists[1] listOptions] & CPTextListPrependEnclosingMarker)];
|
||||
[self assert:nestedLists[0] equals:[self textListsAtIndex:0 ofString:parsed][0] message:@"the nested item stays in the outer list"];
|
||||
[self assert:36.0 equals:[[parsed attribute:CPParagraphStyleAttributeName atIndex:0 effectiveRange:nil] headIndent]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -115,4 +115,15 @@
|
||||
[self assert:[attachment _attachmentView] equals:[[textView textStorage] attribute:_CPAttachmentView atIndex:0 effectiveRange:nil]];
|
||||
}
|
||||
|
||||
- (void)testTableAttachmentArchiving
|
||||
{
|
||||
var table = [[_CPTableTextAttachment alloc] initWithHeaders:[@"Name", @"Value"] rows:[[@"one", @"1"], [@"two", @"2"]] width:300.0],
|
||||
decoded = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:table]];
|
||||
|
||||
[self assertTrue:[decoded isKindOfClass:[_CPTableTextAttachment class]]];
|
||||
[self assert:@"Value" equals:[[[decoded headers] objectAtIndex:1] string]];
|
||||
[self assert:@"two" equals:[[[[decoded rows] objectAtIndex:1] objectAtIndex:0] string]];
|
||||
[self assert:CGRectGetWidth([table frame]) equals:CGRectGetWidth([decoded frame]) message:@"the table keeps its width"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -15,9 +15,9 @@ TextEditor is a document-based test application and a tutorial. When the app use
|
||||
|
||||
### Text features not yet supported
|
||||
|
||||
- **Images in text are not fully tested or complete.** `CPTextAttachment` holds an image as one attachment character, and Edit > Insert Image… and Command-V with an image on the clipboard insert images. Not yet tested: several images inserted at once, copying, pasting and undoing an image within the document, and pasting a screenshot. Not yet implemented: dropping image files on the text view, which needs file access in the drag pasteboard (`_CPDOMDataTransferPasteboard`); pasting images with Edit > Paste from the menu, which reads plain text only; and RTF `\pict` and Markdown `![]()`, so an image is lost when text is copied to another application as RTF.
|
||||
- **Tables can only be inserted from Markdown, and are not yet tested interactively.** The only source of tables is CPMarkdownParser, which builds the private `_CPTableTextAttachment`, a view in the text whose cells are text views of their own. Edit > Insert Table Example inserts a sample through it. Not yet tested: editing in a cell and what Tab does there, deleting the table and undoing, copying and pasting it within the document, narrowing the window, and Format menu commands while a cell is being edited. Known from the code: the table has no archiving, so copying it within the application may fail; nothing calls its `resizeToWidth:`, so it keeps its width of 500 points; and in the sample its right edge and the last row's bottom border are not drawn. A command to insert a table, with a fixed default size and no options, waits on these results.
|
||||
- **Lists are not read from or written to RTF.** `CPTextList` and `textLists` in `CPParagraphStyle` now exist, but the RTF parser and producer do not handle `\listtable`, `\listoverridetable`, `\ls` and `\ilvl`. A list copied as RTF keeps its marker text and indents, but loses its list structure. CPMarkdownParser also writes "- item" as the literal text " • item", with no list.
|
||||
- **Images in text are not fully tested or complete.** `CPTextAttachment` holds an image as one attachment character, and Edit > Insert Image… and Command-V with an image on the clipboard insert images. Not yet tested: several images inserted at once, undoing an image change, and pasting a screenshot. Not yet implemented: dropping image files on the text view, which needs file access in the drag pasteboard (`_CPDOMDataTransferPasteboard`); pasting images with Edit > Paste from the menu, which reads plain text only; and RTF `\pict` and Markdown `![]()`, so an image is lost when text is copied to another application as RTF.
|
||||
- **Tables can only be inserted from Markdown, and are not yet tested interactively.** The only source of tables is CPMarkdownParser, which builds the private `_CPTableTextAttachment`, a view in the text whose cells are text views of their own. Edit > Insert Table Example inserts a sample through it. Not yet tested: what Tab does in a cell, deleting the table and undoing, narrowing the window, and Format menu commands while a cell is being edited. Known from the code: nothing calls its `resizeToWidth:`, so it keeps its width of 500 points; and in the sample its right edge and the last row's bottom border are not drawn. A command to insert a table, with a fixed default size and no options, waits on these results.
|
||||
- **Rich text does not reach native applications.** The RTF producer and parser now handle lists (`\listtable`, `\listoverridetable`, `\ls`, `\ilvl`, and Cocoa's `\levelmarker`), Unicode (`\uN`) and the first line indent as an offset from the left indent, and the unit tests round-trip them. But browsers do not put `text/rtf` from a page's clipboard event on the system clipboard in a form native applications read: text copied from TextEditor with Command-C reaches native TextEdit as plain text. RTF on the clipboard works only between Cappuccino applications in the same browser. Rich text for native applications needs an HTML producer (`text/html`) for the subset the text view makes. Not yet tested: what arrives from native TextEdit with Command-V, in each browser. RTF `\pict` is not implemented. CPMarkdownParser also writes "- item" as the literal text " • item", with no list.
|
||||
|
||||
### Text view
|
||||
|
||||
@@ -88,4 +88,6 @@ How the text system works, for the entries below: `CPTextView` does not use CSS
|
||||
- A wide image fits the text width, and scales with the window without growing beyond its natural size. At the end of the text it is not clipped, and the scroll bars follow it.
|
||||
- Format > Font > Bigger and Smaller (Command-+ and Command--) without a selection change the size of the text typed next, step by step, also in a blank document.
|
||||
- Edit > Insert Table Example: the sample table appears between the surrounding sentences, with its header row and its bold and italic cells.
|
||||
- Copy and paste within TextEditor with Command-C and Command-V, in one document and between documents: images, tables, including text edited in their cells, and lists are kept.
|
||||
- Table cells can be edited, as a first pass.
|
||||
- Builds with both the legacy toolchain and capp-build.
|
||||
Reference in new issue
Block a user