/* * Copyright (C) 2011-2016 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "HTMLConverter.h" #import "ArchiveResource.h" #import "CSSComputedStyleDeclaration.h" #import "CSSParser.h" #import "CSSPrimitiveValue.h" #import "CachedImage.h" #import "CharacterData.h" #import "ColorCocoa.h" #import "ColorMac.h" #import "ComposedTreeIterator.h" #import "CustomHeaderFields.h" #import "Document.h" #import "DocumentLoader.h" #import "Editing.h" #import "Element.h" #import "ElementTraversal.h" #import "File.h" #import "FontCascade.h" #import "Frame.h" #import "FrameLoader.h" #import "HTMLAttachmentElement.h" #import "HTMLElement.h" #import "HTMLFrameElement.h" #import "HTMLIFrameElement.h" #import "HTMLImageElement.h" #import "HTMLInputElement.h" #import "HTMLMetaElement.h" #import "HTMLNames.h" #import "HTMLOListElement.h" #import "HTMLParserIdioms.h" #import "HTMLTableCellElement.h" #import "HTMLTextAreaElement.h" #import "LoaderNSURLExtras.h" #import "RGBColor.h" #import "RenderImage.h" #import "RenderText.h" #import "StyleProperties.h" #import "StyledElement.h" #import "TextIterator.h" #import "VisibleSelection.h" #import <objc/runtime.h> #import <pal/spi/cocoa/NSAttributedStringSPI.h> #import <wtf/ASCIICType.h> #import <wtf/text/StringBuilder.h> #if PLATFORM(IOS_FAMILY) #import "WAKAppKitStubs.h" #import <pal/ios/UIKitSoftLink.h> #import <pal/spi/ios/UIKitSPI.h> SOFT_LINK_CLASS(UIFoundation, NSColor) SOFT_LINK_CLASS(UIFoundation, NSShadow) SOFT_LINK_CLASS(UIFoundation, NSTextAttachment) SOFT_LINK_CLASS(UIFoundation, NSMutableParagraphStyle) SOFT_LINK_CLASS(UIFoundation, NSParagraphStyle) SOFT_LINK_CLASS(UIFoundation, NSTextList) SOFT_LINK_CLASS(UIFoundation, NSTextBlock) SOFT_LINK_CLASS(UIFoundation, NSTextTableBlock) SOFT_LINK_CLASS(UIFoundation, NSTextTable) SOFT_LINK_CLASS(UIFoundation, NSTextTab) #define PlatformNSShadow getNSShadowClass() #define PlatformNSTextAttachment getNSTextAttachmentClass() #define PlatformNSParagraphStyle getNSParagraphStyleClass() #define PlatformNSTextList getNSTextListClass() #define PlatformNSTextTableBlock getNSTextTableBlockClass() #define PlatformNSTextTable getNSTextTableClass() #define PlatformNSTextTab getNSTextTabClass() #define PlatformColor UIColor #define PlatformColorClass PAL::getUIColorClass() #define PlatformNSColorClass getNSColorClass() #define PlatformFont UIFont #define PlatformFontClass PAL::getUIFontClass() #define PlatformImageClass PAL::getUIImageClass() #else #define PlatformNSShadow NSShadow #define PlatformNSTextAttachment NSTextAttachment #define PlatformNSParagraphStyle NSParagraphStyle #define PlatformNSTextList NSTextList #define PlatformNSTextTableBlock NSTextTableBlock #define PlatformNSTextTable NSTextTable #define PlatformNSTextTab NSTextTab #define PlatformColor NSColor #define PlatformColorClass NSColor #define PlatformNSColorClass NSColor #define PlatformFont NSFont #define PlatformFontClass NSFont #define PlatformImageClass NSImage #endif using namespace WebCore; using namespace HTMLNames; #if PLATFORM(IOS_FAMILY) enum { NSTextBlockAbsoluteValueType = 0, // Absolute value in points NSTextBlockPercentageValueType = 1 // Percentage value (out of 100) }; typedef NSUInteger NSTextBlockValueType; enum { NSTextBlockWidth = 0, NSTextBlockMinimumWidth = 1, NSTextBlockMaximumWidth = 2, NSTextBlockHeight = 4, NSTextBlockMinimumHeight = 5, NSTextBlockMaximumHeight = 6 }; typedef NSUInteger NSTextBlockDimension; enum { NSTextBlockPadding = -1, NSTextBlockBorder = 0, NSTextBlockMargin = 1 }; typedef NSInteger NSTextBlockLayer; enum { NSTextTableAutomaticLayoutAlgorithm = 0, NSTextTableFixedLayoutAlgorithm = 1 }; typedef NSUInteger NSTextTableLayoutAlgorithm; enum { NSTextBlockTopAlignment = 0, NSTextBlockMiddleAlignment = 1, NSTextBlockBottomAlignment = 2, NSTextBlockBaselineAlignment = 3 }; typedef NSUInteger NSTextBlockVerticalAlignment; enum { NSEnterCharacter = 0x0003, NSBackspaceCharacter = 0x0008, NSTabCharacter = 0x0009, NSNewlineCharacter = 0x000a, NSFormFeedCharacter = 0x000c, NSCarriageReturnCharacter = 0x000d, NSBackTabCharacter = 0x0019, NSDeleteCharacter = 0x007f, NSLineSeparatorCharacter = 0x2028, NSParagraphSeparatorCharacter = 0x2029, }; enum { NSLeftTabStopType = 0, NSRightTabStopType, NSCenterTabStopType, NSDecimalTabStopType }; typedef NSUInteger NSTextTabType; @interface NSColor : UIColor + (id)colorWithCalibratedRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha; @end @interface NSTextTab () - (id)initWithType:(NSTextTabType)type location:(CGFloat)loc; @end @interface NSParagraphStyle () - (void)setHeaderLevel:(NSInteger)level; - (void)setTextBlocks:(NSArray *)array; @end @interface NSTextBlock : NSObject - (void)setValue:(CGFloat)val type:(NSTextBlockValueType)type forDimension:(NSTextBlockDimension)dimension; - (void)setWidth:(CGFloat)val type:(NSTextBlockValueType)type forLayer:(NSTextBlockLayer)layer edge:(NSRectEdge)edge; - (void)setBackgroundColor:(UIColor *)color; - (UIColor *)backgroundColor; - (void)setBorderColor:(UIColor *)color forEdge:(NSRectEdge)edge; - (void)setBorderColor:(UIColor *)color; // Convenience method sets all edges at once - (void)setVerticalAlignment:(NSTextBlockVerticalAlignment)alignment; @end @interface NSTextTable : NSTextBlock - (void)setNumberOfColumns:(NSUInteger)numCols; - (void)setCollapsesBorders:(BOOL)flag; - (void)setHidesEmptyCells:(BOOL)flag; - (void)setLayoutAlgorithm:(NSTextTableLayoutAlgorithm)algorithm; - (NSUInteger)numberOfColumns; - (void)release; @end @interface NSTextTableBlock : NSTextBlock - (id)initWithTable:(NSTextTable *)table startingRow:(NSInteger)row rowSpan:(NSInteger)rowSpan startingColumn:(NSInteger)col columnSpan:(NSInteger)colSpan; // Designated initializer - (NSInteger)startingColumn; - (NSInteger)startingRow; - (NSUInteger)numberOfColumns; - (NSInteger)columnSpan; - (NSInteger)rowSpan; @end #else static NSFileWrapper *fileWrapperForURL(DocumentLoader *, NSURL *); static RetainPtr<NSFileWrapper> fileWrapperForElement(HTMLImageElement&); @interface NSTextAttachment (WebCoreNSTextAttachment) - (void)setIgnoresOrientation:(BOOL)flag; - (void)setBounds:(CGRect)bounds; - (BOOL)ignoresOrientation; @end #endif // Additional control Unicode characters const unichar WebNextLineCharacter = 0x0085; static const CGFloat defaultFontSize = 12; static const CGFloat minimumFontSize = 1; class HTMLConverterCaches { WTF_MAKE_FAST_ALLOCATED; public: String propertyValueForNode(Node&, CSSPropertyID ); bool floatPropertyValueForNode(Node&, CSSPropertyID, float&); Color colorPropertyValueForNode(Node&, CSSPropertyID); bool isBlockElement(Element&); bool elementHasOwnBackgroundColor(Element&); RefPtr<CSSValue> computedStylePropertyForElement(Element&, CSSPropertyID); RefPtr<CSSValue> inlineStylePropertyForElement(Element&, CSSPropertyID); Node* cacheAncestorsOfStartToBeConverted(const Position&, const Position&); bool isAncestorsOfStartToBeConverted(Node& node) const { return m_ancestorsUnderCommonAncestor.contains(&node); } private: HashMap<Element*, std::unique_ptr<ComputedStyleExtractor>> m_computedStyles; HashSet<Node*> m_ancestorsUnderCommonAncestor; }; @interface NSTextList (WebCoreNSTextListDetails) + (NSDictionary *)_standardMarkerAttributesForAttributes:(NSDictionary *)attrs; @end @interface NSURL (WebCoreNSURLDetails) // FIXME: What is the reason to use this Foundation method, and not +[NSURL URLWithString:relativeToURL:]? + (NSURL *)_web_URLWithString:(NSString *)string relativeToURL:(NSURL *)baseURL; @end @interface NSObject(WebMessageDocumentSimulation) + (void)document:(NSObject **)outDocument attachment:(NSTextAttachment **)outAttachment forURL:(NSURL *)url; @end class HTMLConverter { public: HTMLConverter(const Position&, const Position&); ~HTMLConverter(); NSAttributedString* convert(NSDictionary** documentAttributes = nullptr); private: Position m_start; Position m_end; DocumentLoader* m_dataSource; HashMap<RefPtr<Element>, RetainPtr<NSDictionary>> m_attributesForElements; HashMap<RetainPtr<CFTypeRef>, RefPtr<Element>> m_textTableFooters; HashMap<RefPtr<Element>, RetainPtr<NSDictionary>> m_aggregatedAttributesForElements; NSMutableAttributedString *_attrStr; NSMutableDictionary *_documentAttrs; NSURL *_baseURL; NSMutableArray *_textLists; NSMutableArray *_textBlocks; NSMutableArray *_textTables; NSMutableArray *_textTableSpacings; NSMutableArray *_textTablePaddings; NSMutableArray *_textTableRows; NSMutableArray *_textTableRowArrays; NSMutableArray *_textTableRowBackgroundColors; NSMutableDictionary *_fontCache; NSMutableArray *_writingDirectionArray; CGFloat _defaultTabInterval; NSUInteger _domRangeStartIndex; NSInteger _quoteLevel; std::unique_ptr<HTMLConverterCaches> _caches; struct { unsigned int isSoft:1; unsigned int reachedStart:1; unsigned int reachedEnd:1; unsigned int hasTrailingNewline:1; unsigned int pad:26; } _flags; PlatformColor *_colorForElement(Element&, CSSPropertyID); void _traverseNode(Node&, unsigned depth, bool embedded); void _traverseFooterNode(Element&, unsigned depth); NSDictionary *computedAttributesForElement(Element&); NSDictionary *attributesForElement(Element&); NSDictionary *aggregatedAttributesForAncestors(CharacterData&); NSDictionary* aggregatedAttributesForElementAndItsAncestors(Element&); Element* _blockLevelElementForNode(Node*); void _newParagraphForElement(Element&, NSString *tag, BOOL flag, BOOL suppressTrailingSpace); void _newLineForElement(Element&); void _newTabForElement(Element&); BOOL _addAttachmentForElement(Element&, NSURL *url, BOOL needsParagraph, BOOL usePlaceholder); void _addQuoteForElement(Element&, BOOL opening, NSInteger level); void _addValue(NSString *value, Element&); void _fillInBlock(NSTextBlock *block, Element&, PlatformColor *backgroundColor, CGFloat extraMargin, CGFloat extraPadding, BOOL isTable); BOOL _enterElement(Element&, BOOL embedded); BOOL _processElement(Element&, NSInteger depth); void _exitElement(Element&, NSInteger depth, NSUInteger startIndex); void _processHeadElement(Element&); void _processMetaElementWithName(NSString *name, NSString *content); void _addTableForElement(Element* tableElement); void _addTableCellForElement(Element* tableCellElement); void _addMarkersToList(NSTextList *list, NSRange range); void _processText(CharacterData&); void _adjustTrailingNewline(); }; HTMLConverter::HTMLConverter(const Position& start, const Position& end) : m_start(start) , m_end(end) , m_dataSource(nullptr) { _attrStr = [[NSMutableAttributedString alloc] init]; _documentAttrs = [[NSMutableDictionary alloc] init]; _baseURL = nil; _textLists = [[NSMutableArray alloc] init]; _textBlocks = [[NSMutableArray alloc] init]; _textTables = [[NSMutableArray alloc] init]; _textTableSpacings = [[NSMutableArray alloc] init]; _textTablePaddings = [[NSMutableArray alloc] init]; _textTableRows = [[NSMutableArray alloc] init]; _textTableRowArrays = [[NSMutableArray alloc] init]; _textTableRowBackgroundColors = [[NSMutableArray alloc] init]; _fontCache = [[NSMutableDictionary alloc] init]; _writingDirectionArray = [[NSMutableArray alloc] init]; _defaultTabInterval = 36; _domRangeStartIndex = 0; _quoteLevel = 0; _flags.isSoft = false; _flags.reachedStart = false; _flags.reachedEnd = false; _caches = makeUnique<HTMLConverterCaches>(); } HTMLConverter::~HTMLConverter() { [_attrStr release]; [_documentAttrs release]; [_textLists release]; [_textBlocks release]; [_textTables release]; [_textTableSpacings release]; [_textTablePaddings release]; [_textTableRows release]; [_textTableRowArrays release]; [_textTableRowBackgroundColors release]; [_fontCache release]; [_writingDirectionArray release]; } NSAttributedString *HTMLConverter::convert(NSDictionary** documentAttributes) { if (comparePositions(m_start, m_end) > 0) return nil; Node* commonAncestorContainer = _caches->cacheAncestorsOfStartToBeConverted(m_start, m_end); ASSERT(commonAncestorContainer); m_dataSource = commonAncestorContainer->document().frame()->loader().documentLoader(); Document& document = commonAncestorContainer->document(); if (auto* body = document.bodyOrFrameset()) { if (PlatformColor *backgroundColor = _colorForElement(*body, CSSPropertyBackgroundColor)) [_documentAttrs setObject:backgroundColor forKey:NSBackgroundColorDocumentAttribute]; } _domRangeStartIndex = 0; _traverseNode(*commonAncestorContainer, 0, false /* embedded */); if (_domRangeStartIndex > 0 && _domRangeStartIndex <= [_attrStr length]) [_attrStr deleteCharactersInRange:NSMakeRange(0, _domRangeStartIndex)]; if (documentAttributes) *documentAttributes = [[_documentAttrs retain] autorelease]; return [[_attrStr retain] autorelease]; } #if !PLATFORM(IOS_FAMILY) // Returns the font to be used if the NSFontAttributeName doesn't exist static NSFont *WebDefaultFont() { static NSFont *defaultFont = nil; if (defaultFont) return defaultFont; NSFont *font = [NSFont fontWithName:@"Helvetica" size:12]; if (!font) font = [NSFont systemFontOfSize:12]; defaultFont = [font retain]; return defaultFont; } #endif static PlatformFont *_fontForNameAndSize(NSString *fontName, CGFloat size, NSMutableDictionary *cache) { PlatformFont *font = [cache objectForKey:fontName]; #if PLATFORM(IOS_FAMILY) if (font) return [font fontWithSize:size]; font = [PlatformFontClass fontWithName:fontName size:size]; #else NSFontManager *fontManager = [NSFontManager sharedFontManager]; if (font) { font = [fontManager convertFont:font toSize:size]; return font; } font = [fontManager fontWithFamily:fontName traits:0 weight:0 size:size]; #endif if (!font) { #if PLATFORM(IOS_FAMILY) NSArray *availableFamilyNames = [PlatformFontClass familyNames]; #else NSArray *availableFamilyNames = [fontManager availableFontFamilies]; #endif NSRange dividingRange; NSRange dividingSpaceRange = [fontName rangeOfString:@" " options:NSBackwardsSearch]; NSRange dividingDashRange = [fontName rangeOfString:@"-" options:NSBackwardsSearch]; dividingRange = (0 < dividingSpaceRange.length && 0 < dividingDashRange.length) ? (dividingSpaceRange.location > dividingDashRange.location ? dividingSpaceRange : dividingDashRange) : (0 < dividingSpaceRange.length ? dividingSpaceRange : dividingDashRange); while (dividingRange.length > 0) { NSString *familyName = [fontName substringToIndex:dividingRange.location]; if ([availableFamilyNames containsObject:familyName]) { #if PLATFORM(IOS_FAMILY) NSString *faceName = [fontName substringFromIndex:(dividingRange.location + dividingRange.length)]; NSArray *familyMemberFaceNames = [PlatformFontClass fontNamesForFamilyName:familyName]; for (NSString *familyMemberFaceName in familyMemberFaceNames) { if ([familyMemberFaceName compare:faceName options:NSCaseInsensitiveSearch] == NSOrderedSame) { font = [PlatformFontClass fontWithName:familyMemberFaceName size:size]; break; } } if (!font && [familyMemberFaceNames count]) font = [PlatformFontClass fontWithName:familyName size:size]; #else NSArray *familyMemberArray; NSString *faceName = [fontName substringFromIndex:(dividingRange.location + dividingRange.length)]; NSArray *familyMemberArrays = [fontManager availableMembersOfFontFamily:familyName]; NSEnumerator *familyMemberArraysEnum = [familyMemberArrays objectEnumerator]; while ((familyMemberArray = [familyMemberArraysEnum nextObject])) { NSString *familyMemberFaceName = [familyMemberArray objectAtIndex:1]; if ([familyMemberFaceName compare:faceName options:NSCaseInsensitiveSearch] == NSOrderedSame) { NSFontTraitMask traits = [[familyMemberArray objectAtIndex:3] integerValue]; NSInteger weight = [[familyMemberArray objectAtIndex:2] integerValue]; font = [fontManager fontWithFamily:familyName traits:traits weight:weight size:size]; break; } } if (!font) { if (0 < [familyMemberArrays count]) { NSArray *familyMemberArray = [familyMemberArrays objectAtIndex:0]; NSFontTraitMask traits = [[familyMemberArray objectAtIndex:3] integerValue]; NSInteger weight = [[familyMemberArray objectAtIndex:2] integerValue]; font = [fontManager fontWithFamily:familyName traits:traits weight:weight size:size]; } } #endif break; } else { dividingSpaceRange = [familyName rangeOfString:@" " options:NSBackwardsSearch]; dividingDashRange = [familyName rangeOfString:@"-" options:NSBackwardsSearch]; dividingRange = (0 < dividingSpaceRange.length && 0 < dividingDashRange.length) ? (dividingSpaceRange.location > dividingDashRange.location ? dividingSpaceRange : dividingDashRange) : (0 < dividingSpaceRange.length ? dividingSpaceRange : dividingDashRange); } } } #if PLATFORM(IOS_FAMILY) if (!font) font = [PlatformFontClass systemFontOfSize:size]; #else if (!font) font = [NSFont fontWithName:@"Times" size:size]; if (!font) font = [NSFont userFontOfSize:size]; if (!font) font = [fontManager convertFont:WebDefaultFont() toSize:size]; if (!font) font = WebDefaultFont(); #endif [cache setObject:font forKey:fontName]; return font; } static NSParagraphStyle *defaultParagraphStyle() { static NSMutableParagraphStyle *defaultParagraphStyle = nil; if (!defaultParagraphStyle) { defaultParagraphStyle = [[PlatformNSParagraphStyle defaultParagraphStyle] mutableCopy]; [defaultParagraphStyle setDefaultTabInterval:36]; [defaultParagraphStyle setTabStops:[NSArray array]]; } return defaultParagraphStyle; } RefPtr<CSSValue> HTMLConverterCaches::computedStylePropertyForElement(Element& element, CSSPropertyID propertyId) { if (propertyId == CSSPropertyInvalid) return nullptr; auto result = m_computedStyles.add(&element, nullptr); if (result.isNewEntry) result.iterator->value = makeUnique<ComputedStyleExtractor>(&element, true); ComputedStyleExtractor& computedStyle = *result.iterator->value; return computedStyle.propertyValue(propertyId); } RefPtr<CSSValue> HTMLConverterCaches::inlineStylePropertyForElement(Element& element, CSSPropertyID propertyId) { if (propertyId == CSSPropertyInvalid || !is<StyledElement>(element)) return nullptr; const StyleProperties* properties = downcast<StyledElement>(element).inlineStyle(); if (!properties) return nullptr; return properties->getPropertyCSSValue(propertyId); } static bool stringFromCSSValue(CSSValue& value, String& result) { if (is<CSSPrimitiveValue>(value)) { // FIXME: Use isStringType(CSSUnitType)? CSSUnitType primitiveType = downcast<CSSPrimitiveValue>(value).primitiveType(); if (primitiveType == CSSUnitType::CSS_STRING || primitiveType == CSSUnitType::CSS_URI || primitiveType == CSSUnitType::CSS_IDENT || primitiveType == CSSUnitType::CSS_ATTR) { String stringValue = value.cssText(); if (stringValue.length()) { result = stringValue; return true; } } } else if (value.isValueList()) { result = value.cssText(); return true; } return false; } String HTMLConverterCaches::propertyValueForNode(Node& node, CSSPropertyID propertyId) { if (!is<Element>(node)) { if (Node* parent = node.parentInComposedTree()) return propertyValueForNode(*parent, propertyId); return String(); } bool inherit = false; Element& element = downcast<Element>(node); if (RefPtr<CSSValue> value = computedStylePropertyForElement(element, propertyId)) { String result; if (stringFromCSSValue(*value, result)) return result; } if (RefPtr<CSSValue> value = inlineStylePropertyForElement(element, propertyId)) { String result; if (value->isInheritedValue()) inherit = true; else if (stringFromCSSValue(*value, result)) return result; } switch (propertyId) { case CSSPropertyDisplay: if (element.hasTagName(headTag) || element.hasTagName(scriptTag) || element.hasTagName(appletTag) || element.hasTagName(noframesTag)) return "none"; else if (element.hasTagName(addressTag) || element.hasTagName(blockquoteTag) || element.hasTagName(bodyTag) || element.hasTagName(centerTag) || element.hasTagName(ddTag) || element.hasTagName(dirTag) || element.hasTagName(divTag) || element.hasTagName(dlTag) || element.hasTagName(dtTag) || element.hasTagName(fieldsetTag) || element.hasTagName(formTag) || element.hasTagName(frameTag) || element.hasTagName(framesetTag) || element.hasTagName(hrTag) || element.hasTagName(htmlTag) || element.hasTagName(h1Tag) || element.hasTagName(h2Tag) || element.hasTagName(h3Tag) || element.hasTagName(h4Tag) || element.hasTagName(h5Tag) || element.hasTagName(h6Tag) || element.hasTagName(iframeTag) || element.hasTagName(menuTag) || element.hasTagName(noscriptTag) || element.hasTagName(olTag) || element.hasTagName(pTag) || element.hasTagName(preTag) || element.hasTagName(ulTag)) return "block"; else if (element.hasTagName(liTag)) return "list-item"; else if (element.hasTagName(tableTag)) return "table"; else if (element.hasTagName(trTag)) return "table-row"; else if (element.hasTagName(thTag) || element.hasTagName(tdTag)) return "table-cell"; else if (element.hasTagName(theadTag)) return "table-header-group"; else if (element.hasTagName(tbodyTag)) return "table-row-group"; else if (element.hasTagName(tfootTag)) return "table-footer-group"; else if (element.hasTagName(colTag)) return "table-column"; else if (element.hasTagName(colgroupTag)) return "table-column-group"; else if (element.hasTagName(captionTag)) return "table-caption"; break; case CSSPropertyWhiteSpace: if (element.hasTagName(preTag)) return "pre"; inherit = true; break; case CSSPropertyFontStyle: if (element.hasTagName(iTag) || element.hasTagName(citeTag) || element.hasTagName(emTag) || element.hasTagName(varTag) || element.hasTagName(addressTag)) return "italic"; inherit = true; break; case CSSPropertyFontWeight: if (element.hasTagName(bTag) || element.hasTagName(strongTag) || element.hasTagName(thTag)) return "bolder"; inherit = true; break; case CSSPropertyTextDecoration: if (element.hasTagName(uTag) || element.hasTagName(insTag)) return "underline"; else if (element.hasTagName(sTag) || element.hasTagName(strikeTag) || element.hasTagName(delTag)) return "line-through"; inherit = true; // FIXME: This is not strictly correct break; case CSSPropertyTextAlign: if (element.hasTagName(centerTag) || element.hasTagName(captionTag) || element.hasTagName(thTag)) return "center"; inherit = true; break; case CSSPropertyVerticalAlign: if (element.hasTagName(supTag)) return "super"; else if (element.hasTagName(subTag)) return "sub"; else if (element.hasTagName(theadTag) || element.hasTagName(tbodyTag) || element.hasTagName(tfootTag)) return "middle"; else if (element.hasTagName(trTag) || element.hasTagName(thTag) || element.hasTagName(tdTag)) inherit = true; break; case CSSPropertyFontFamily: case CSSPropertyFontVariantCaps: case CSSPropertyTextTransform: case CSSPropertyTextShadow: case CSSPropertyVisibility: case CSSPropertyBorderCollapse: case CSSPropertyEmptyCells: case CSSPropertyWordSpacing: case CSSPropertyListStyleType: case CSSPropertyDirection: inherit = true; // FIXME: Let classes in the css component figure this out. break; default: break; } if (inherit) { if (Node* parent = node.parentInComposedTree()) return propertyValueForNode(*parent, propertyId); } return String(); } static inline bool floatValueFromPrimitiveValue(CSSPrimitiveValue& primitiveValue, float& result) { // FIXME: Use CSSPrimitiveValue::computeValue. switch (primitiveValue.primitiveType()) { case CSSUnitType::CSS_PX: result = primitiveValue.floatValue(CSSUnitType::CSS_PX); return true; case CSSUnitType::CSS_PT: result = 4 * primitiveValue.floatValue(CSSUnitType::CSS_PT) / 3; return true; case CSSUnitType::CSS_PC: result = 16 * primitiveValue.floatValue(CSSUnitType::CSS_PC); return true; case CSSUnitType::CSS_CM: result = 96 * primitiveValue.floatValue(CSSUnitType::CSS_PC) / 2.54; return true; case CSSUnitType::CSS_MM: result = 96 * primitiveValue.floatValue(CSSUnitType::CSS_PC) / 25.4; return true; case CSSUnitType::CSS_Q: result = 96 * primitiveValue.floatValue(CSSUnitType::CSS_PC) / (25.4 * 4.0); return true; case CSSUnitType::CSS_IN: result = 96 * primitiveValue.floatValue(CSSUnitType::CSS_IN); return true; default: return false; } } bool HTMLConverterCaches::floatPropertyValueForNode(Node& node, CSSPropertyID propertyId, float& result) { if (!is<Element>(node)) { if (ContainerNode* parent = node.parentInComposedTree()) return floatPropertyValueForNode(*parent, propertyId, result); return false; } Element& element = downcast<Element>(node); if (RefPtr<CSSValue> value = computedStylePropertyForElement(element, propertyId)) { if (is<CSSPrimitiveValue>(*value) && floatValueFromPrimitiveValue(downcast<CSSPrimitiveValue>(*value), result)) return true; } bool inherit = false; if (RefPtr<CSSValue> value = inlineStylePropertyForElement(element, propertyId)) { if (is<CSSPrimitiveValue>(*value) && floatValueFromPrimitiveValue(downcast<CSSPrimitiveValue>(*value), result)) return true; if (value->isInheritedValue()) inherit = true; } switch (propertyId) { case CSSPropertyTextIndent: case CSSPropertyLetterSpacing: case CSSPropertyWordSpacing: case CSSPropertyLineHeight: case CSSPropertyWidows: case CSSPropertyOrphans: inherit = true; break; default: break; } if (inherit) { if (ContainerNode* parent = node.parentInComposedTree()) return floatPropertyValueForNode(*parent, propertyId, result); } return false; } static inline NSShadow *_shadowForShadowStyle(NSString *shadowStyle) { NSShadow *shadow = nil; NSUInteger shadowStyleLength = [shadowStyle length]; NSRange openParenRange = [shadowStyle rangeOfString:@"("]; NSRange closeParenRange = [shadowStyle rangeOfString:@")"]; NSRange firstRange = NSMakeRange(NSNotFound, 0); NSRange secondRange = NSMakeRange(NSNotFound, 0); NSRange thirdRange = NSMakeRange(NSNotFound, 0); NSRange spaceRange; if (openParenRange.length > 0 && closeParenRange.length > 0 && NSMaxRange(openParenRange) < closeParenRange.location) { NSArray *components = [[shadowStyle substringWithRange:NSMakeRange(NSMaxRange(openParenRange), closeParenRange.location - NSMaxRange(openParenRange))] componentsSeparatedByString:@","]; if ([components count] >= 3) { CGFloat red = [[components objectAtIndex:0] floatValue] / 255; CGFloat green = [[components objectAtIndex:1] floatValue] / 255; CGFloat blue = [[components objectAtIndex:2] floatValue] / 255; CGFloat alpha = ([components count] >= 4) ? [[components objectAtIndex:3] floatValue] / 255 : 1; NSColor *shadowColor = [PlatformNSColorClass colorWithCalibratedRed:red green:green blue:blue alpha:alpha]; NSSize shadowOffset; CGFloat shadowBlurRadius; firstRange = [shadowStyle rangeOfString:@"px"]; if (firstRange.length > 0 && NSMaxRange(firstRange) < shadowStyleLength) secondRange = [shadowStyle rangeOfString:@"px" options:0 range:NSMakeRange(NSMaxRange(firstRange), shadowStyleLength - NSMaxRange(firstRange))]; if (secondRange.length > 0 && NSMaxRange(secondRange) < shadowStyleLength) thirdRange = [shadowStyle rangeOfString:@"px" options:0 range:NSMakeRange(NSMaxRange(secondRange), shadowStyleLength - NSMaxRange(secondRange))]; if (firstRange.location > 0 && firstRange.length > 0 && secondRange.length > 0 && thirdRange.length > 0) { spaceRange = [shadowStyle rangeOfString:@" " options:NSBackwardsSearch range:NSMakeRange(0, firstRange.location)]; if (spaceRange.length == 0) spaceRange = NSMakeRange(0, 0); shadowOffset.width = [[shadowStyle substringWithRange:NSMakeRange(NSMaxRange(spaceRange), firstRange.location - NSMaxRange(spaceRange))] floatValue]; spaceRange = [shadowStyle rangeOfString:@" " options:NSBackwardsSearch range:NSMakeRange(0, secondRange.location)]; if (!spaceRange.length) spaceRange = NSMakeRange(0, 0); CGFloat shadowHeight = [[shadowStyle substringWithRange:NSMakeRange(NSMaxRange(spaceRange), secondRange.location - NSMaxRange(spaceRange))] floatValue]; // I don't know why we have this difference between the two platforms. #if PLATFORM(IOS_FAMILY) shadowOffset.height = shadowHeight; #else shadowOffset.height = -shadowHeight; #endif spaceRange = [shadowStyle rangeOfString:@" " options:NSBackwardsSearch range:NSMakeRange(0, thirdRange.location)]; if (!spaceRange.length) spaceRange = NSMakeRange(0, 0); shadowBlurRadius = [[shadowStyle substringWithRange:NSMakeRange(NSMaxRange(spaceRange), thirdRange.location - NSMaxRange(spaceRange))] floatValue]; shadow = [[(NSShadow *)[PlatformNSShadow alloc] init] autorelease]; [shadow setShadowColor:shadowColor]; [shadow setShadowOffset:shadowOffset]; [shadow setShadowBlurRadius:shadowBlurRadius]; } } } return shadow; } bool HTMLConverterCaches::isBlockElement(Element& element) { String displayValue = propertyValueForNode(element, CSSPropertyDisplay); if (displayValue == "block" || displayValue == "list-item" || displayValue.startsWith("table")) return true; String floatValue = propertyValueForNode(element, CSSPropertyFloat); if (floatValue == "left" || floatValue == "right") return true; return false; } bool HTMLConverterCaches::elementHasOwnBackgroundColor(Element& element) { if (!isBlockElement(element)) return false; // In the text system, text blocks (table elements) and documents (body elements) // have their own background colors, which should not be inherited. return element.hasTagName(htmlTag) || element.hasTagName(bodyTag) || propertyValueForNode(element, CSSPropertyDisplay).startsWith("table"); } Element* HTMLConverter::_blockLevelElementForNode(Node* node) { Element* element = node->parentElement(); if (element && !_caches->isBlockElement(*element)) element = _blockLevelElementForNode(element->parentInComposedTree()); return element; } static Color normalizedColor(Color color, bool ignoreDefaultColor, Element& element) { if (!ignoreDefaultColor) return color; bool useDarkAppearance = element.document().useDarkAppearance(element.existingComputedStyle()); if (useDarkAppearance && Color::isWhiteColor(color)) return Color(); if (!useDarkAppearance && Color::isBlackColor(color)) return Color(); return color; } Color HTMLConverterCaches::colorPropertyValueForNode(Node& node, CSSPropertyID propertyId) { if (!is<Element>(node)) { if (Node* parent = node.parentInComposedTree()) return colorPropertyValueForNode(*parent, propertyId); return Color(); } bool ignoreDefaultColor = propertyId == CSSPropertyColor; Element& element = downcast<Element>(node); if (RefPtr<CSSValue> value = computedStylePropertyForElement(element, propertyId)) { if (is<CSSPrimitiveValue>(*value) && downcast<CSSPrimitiveValue>(*value).isRGBColor()) return normalizedColor(Color(downcast<CSSPrimitiveValue>(*value).color().rgb()), ignoreDefaultColor, element); } bool inherit = false; if (RefPtr<CSSValue> value = inlineStylePropertyForElement(element, propertyId)) { if (is<CSSPrimitiveValue>(*value) && downcast<CSSPrimitiveValue>(*value).isRGBColor()) return normalizedColor(Color(downcast<CSSPrimitiveValue>(*value).color().rgb()), ignoreDefaultColor, element); if (value->isInheritedValue()) inherit = true; } switch (propertyId) { case CSSPropertyColor: inherit = true; break; case CSSPropertyBackgroundColor: if (!elementHasOwnBackgroundColor(element)) { if (Element* parentElement = node.parentElement()) { if (!elementHasOwnBackgroundColor(*parentElement)) inherit = true; } } break; default: break; } if (inherit) { if (Node* parent = node.parentInComposedTree()) return colorPropertyValueForNode(*parent, propertyId); } return Color(); } PlatformColor *HTMLConverter::_colorForElement(Element& element, CSSPropertyID propertyId) { Color result = _caches->colorPropertyValueForNode(element, propertyId); if (!result.isValid()) return nil; PlatformColor *platformResult = platformColor(result); if ([[PlatformColorClass clearColor] isEqual:platformResult] || ([platformResult alphaComponent] == 0.0)) return nil; return platformResult; } static PlatformFont *_font(Element& element) { auto* renderer = element.renderer(); if (!renderer) return nil; return (__bridge PlatformFont *)renderer->style().fontCascade().primaryFont().getCTFont(); } #define UIFloatIsZero(number) (fabs(number - 0) < FLT_EPSILON) NSDictionary *HTMLConverter::computedAttributesForElement(Element& element) { NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; #if !PLATFORM(IOS_FAMILY) NSFontManager *fontManager = [NSFontManager sharedFontManager]; #endif PlatformFont *font = nil; PlatformFont *actualFont = _font(element); PlatformColor *foregroundColor = _colorForElement(element, CSSPropertyColor); PlatformColor *backgroundColor = _colorForElement(element, CSSPropertyBackgroundColor); PlatformColor *strokeColor = _colorForElement(element, CSSPropertyWebkitTextStrokeColor); float fontSize = 0; if (!_caches->floatPropertyValueForNode(element, CSSPropertyFontSize, fontSize) || fontSize <= 0.0) fontSize = defaultFontSize; if (fontSize < minimumFontSize) fontSize = minimumFontSize; if (fabs(floor(2.0 * fontSize + 0.5) / 2.0 - fontSize) < 0.05) fontSize = floor(2.0 * fontSize + 0.5) / 2; else if (fabs(floor(10.0 * fontSize + 0.5) / 10.0 - fontSize) < 0.005) fontSize = floor(10.0 * fontSize + 0.5) / 10; if (fontSize <= 0.0) fontSize = defaultFontSize; #if PLATFORM(IOS_FAMILY) if (actualFont) font = [actualFont fontWithSize:fontSize]; #else if (actualFont) font = [fontManager convertFont:actualFont toSize:fontSize]; #endif if (!font) { String fontName = _caches->propertyValueForNode(element, CSSPropertyFontFamily); if (fontName.length()) font = _fontForNameAndSize(fontName.convertToASCIILowercase(), fontSize, _fontCache); if (!font) font = [PlatformFontClass fontWithName:@"Times" size:fontSize]; String fontStyle = _caches->propertyValueForNode(element, CSSPropertyFontStyle); if (fontStyle == "italic" || fontStyle == "oblique") { PlatformFont *originalFont = font; #if PLATFORM(IOS_FAMILY) font = [PlatformFontClass fontWithFamilyName:[font familyName] traits:UIFontTraitItalic size:[font pointSize]]; #else font = [fontManager convertFont:font toHaveTrait:NSItalicFontMask]; #endif if (!font) font = originalFont; } String fontWeight = _caches->propertyValueForNode(element, CSSPropertyFontStyle); if (fontWeight.startsWith("bold") || fontWeight.toInt() >= 700) { // ??? handle weight properly using NSFontManager PlatformFont *originalFont = font; #if PLATFORM(IOS_FAMILY) font = [PlatformFontClass fontWithFamilyName:[font familyName] traits:UIFontTraitBold size:[font pointSize]]; #else font = [fontManager convertFont:font toHaveTrait:NSBoldFontMask]; #endif if (!font) font = originalFont; } #if !PLATFORM(IOS_FAMILY) // IJB: No small caps support on iOS if (_caches->propertyValueForNode(element, CSSPropertyFontVariantCaps) == "small-caps") { // ??? synthesize small-caps if [font isEqual:originalFont] NSFont *originalFont = font; font = [fontManager convertFont:font toHaveTrait:NSSmallCapsFontMask]; if (!font) font = originalFont; } #endif } if (font) [attrs setObject:font forKey:NSFontAttributeName]; if (foregroundColor) [attrs setObject:foregroundColor forKey:NSForegroundColorAttributeName]; if (backgroundColor && !_caches->elementHasOwnBackgroundColor(element)) [attrs setObject:backgroundColor forKey:NSBackgroundColorAttributeName]; float strokeWidth = 0.0; if (_caches->floatPropertyValueForNode(element, CSSPropertyWebkitTextStrokeWidth, strokeWidth)) { float textStrokeWidth = strokeWidth / ([font pointSize] * 0.01); [attrs setObject:[NSNumber numberWithDouble:textStrokeWidth] forKey:NSStrokeWidthAttributeName]; } if (strokeColor) [attrs setObject:strokeColor forKey:NSStrokeColorAttributeName]; String fontKerning = _caches->propertyValueForNode(element, CSSPropertyWebkitFontKerning); String letterSpacing = _caches->propertyValueForNode(element, CSSPropertyLetterSpacing); if (fontKerning.length() || letterSpacing.length()) { if (fontKerning == "none") [attrs setObject:@0.0 forKey:NSKernAttributeName]; else { double kernVal = letterSpacing.length() ? letterSpacing.toDouble() : 0.0; if (UIFloatIsZero(kernVal)) [attrs setObject:@0.0 forKey:NSKernAttributeName]; // auto and normal, the other possible values, are both "kerning enabled" else [attrs setObject:[NSNumber numberWithDouble:kernVal] forKey:NSKernAttributeName]; } } String fontLigatures = _caches->propertyValueForNode(element, CSSPropertyFontVariantLigatures); if (fontLigatures.length()) { if (fontLigatures.contains("normal")) ; // default: whatever the system decides to do else if (fontLigatures.contains("common-ligatures")) [attrs setObject:@1 forKey:NSLigatureAttributeName]; // explicitly enabled else if (fontLigatures.contains("no-common-ligatures")) [attrs setObject:@0 forKey:NSLigatureAttributeName]; // explicitly disabled } String textDecoration = _caches->propertyValueForNode(element, CSSPropertyTextDecoration); if (textDecoration.length()) { if (textDecoration.contains("underline")) [attrs setObject:[NSNumber numberWithInteger:NSUnderlineStyleSingle] forKey:NSUnderlineStyleAttributeName]; if (textDecoration.contains("line-through")) [attrs setObject:[NSNumber numberWithInteger:NSUnderlineStyleSingle] forKey:NSStrikethroughStyleAttributeName]; } String verticalAlign = _caches->propertyValueForNode(element, CSSPropertyVerticalAlign); if (verticalAlign.length()) { if (verticalAlign == "super") [attrs setObject:[NSNumber numberWithInteger:1] forKey:NSSuperscriptAttributeName]; else if (verticalAlign == "sub") [attrs setObject:[NSNumber numberWithInteger:-1] forKey:NSSuperscriptAttributeName]; } float baselineOffset = 0.0; if (_caches->floatPropertyValueForNode(element, CSSPropertyVerticalAlign, baselineOffset)) [attrs setObject:[NSNumber numberWithDouble:baselineOffset] forKey:NSBaselineOffsetAttributeName]; String textShadow = _caches->propertyValueForNode(element, CSSPropertyTextShadow); if (textShadow.length() > 4) { NSShadow *shadow = _shadowForShadowStyle(textShadow); if (shadow) [attrs setObject:shadow forKey:NSShadowAttributeName]; } Element* blockElement = _blockLevelElementForNode(&element); if (&element != blockElement && [_writingDirectionArray count] > 0) [attrs setObject:[NSArray arrayWithArray:_writingDirectionArray] forKey:NSWritingDirectionAttributeName]; if (blockElement) { Element& coreBlockElement = *blockElement; NSMutableParagraphStyle *paragraphStyle = [defaultParagraphStyle() mutableCopy]; unsigned heading = 0; if (coreBlockElement.hasTagName(h1Tag)) heading = 1; else if (coreBlockElement.hasTagName(h2Tag)) heading = 2; else if (coreBlockElement.hasTagName(h3Tag)) heading = 3; else if (coreBlockElement.hasTagName(h4Tag)) heading = 4; else if (coreBlockElement.hasTagName(h5Tag)) heading = 5; else if (coreBlockElement.hasTagName(h6Tag)) heading = 6; bool isParagraph = coreBlockElement.hasTagName(pTag) || coreBlockElement.hasTagName(liTag) || heading; String textAlign = _caches->propertyValueForNode(coreBlockElement, CSSPropertyTextAlign); if (textAlign.length()) { // WebKit can return -khtml-left, -khtml-right, -khtml-center if (textAlign.endsWith("left")) [paragraphStyle setAlignment:NSTextAlignmentLeft]; else if (textAlign.endsWith("right")) [paragraphStyle setAlignment:NSTextAlignmentRight]; else if (textAlign.endsWith("center")) [paragraphStyle setAlignment:NSTextAlignmentCenter]; else if (textAlign.endsWith("justify")) [paragraphStyle setAlignment:NSTextAlignmentJustified]; } String direction = _caches->propertyValueForNode(coreBlockElement, CSSPropertyDirection); if (direction.length()) { if (direction == "ltr") [paragraphStyle setBaseWritingDirection:NSWritingDirectionLeftToRight]; else if (direction == "rtl") [paragraphStyle setBaseWritingDirection:NSWritingDirectionRightToLeft]; } String hyphenation = _caches->propertyValueForNode(coreBlockElement, CSSPropertyWebkitHyphens); if (hyphenation.length()) { if (hyphenation == "auto") [paragraphStyle setHyphenationFactor:1.0]; else [paragraphStyle setHyphenationFactor:0.0]; } if (heading) [paragraphStyle setHeaderLevel:heading]; if (isParagraph) { // FIXME: Why are we ignoring margin-top? float marginLeft = 0.0; if (_caches->floatPropertyValueForNode(coreBlockElement, CSSPropertyMarginLeft, marginLeft) && marginLeft > 0.0) [paragraphStyle setHeadIndent:marginLeft]; float textIndent = 0.0; if (_caches->floatPropertyValueForNode(coreBlockElement, CSSPropertyTextIndent, textIndent) && textIndent > 0.0) [paragraphStyle setFirstLineHeadIndent:[paragraphStyle headIndent] + textIndent]; float marginRight = 0.0; if (_caches->floatPropertyValueForNode(coreBlockElement, CSSPropertyMarginRight, marginRight) && marginRight > 0.0) [paragraphStyle setTailIndent:-marginRight]; float marginBottom = 0.0; if (_caches->floatPropertyValueForNode(coreBlockElement, CSSPropertyMarginRight, marginBottom) && marginBottom > 0.0) [paragraphStyle setParagraphSpacing:marginBottom]; } if ([_textLists count] > 0) [paragraphStyle setTextLists:_textLists]; if ([_textBlocks count] > 0) [paragraphStyle setTextBlocks:_textBlocks]; [attrs setObject:paragraphStyle forKey:NSParagraphStyleAttributeName]; [paragraphStyle release]; } return attrs; } NSDictionary* HTMLConverter::attributesForElement(Element& element) { auto& attributes = m_attributesForElements.add(&element, nullptr).iterator->value; if (!attributes) attributes = computedAttributesForElement(element); return attributes.get(); } NSDictionary* HTMLConverter::aggregatedAttributesForAncestors(CharacterData& node) { Node* ancestor = node.parentInComposedTree(); while (ancestor && !is<Element>(*ancestor)) ancestor = ancestor->parentInComposedTree(); if (!ancestor) return nullptr; return aggregatedAttributesForElementAndItsAncestors(downcast<Element>(*ancestor)); } NSDictionary* HTMLConverter::aggregatedAttributesForElementAndItsAncestors(Element& element) { auto& cachedAttributes = m_aggregatedAttributesForElements.add(&element, nullptr).iterator->value; if (cachedAttributes) return cachedAttributes.get(); NSDictionary* attributesForCurrentElement = attributesForElement(element); ASSERT(attributesForCurrentElement); Node* ancestor = element.parentInComposedTree(); while (ancestor && !is<Element>(*ancestor)) ancestor = ancestor->parentInComposedTree(); if (!ancestor) { cachedAttributes = attributesForCurrentElement; return attributesForCurrentElement; } RetainPtr<NSMutableDictionary> attributesForAncestors = adoptNS([aggregatedAttributesForElementAndItsAncestors(downcast<Element>(*ancestor)) mutableCopy]); [attributesForAncestors addEntriesFromDictionary:attributesForCurrentElement]; m_aggregatedAttributesForElements.set(&element, attributesForAncestors); return attributesForAncestors.get(); } void HTMLConverter::_newParagraphForElement(Element& element, NSString *tag, BOOL flag, BOOL suppressTrailingSpace) { NSUInteger textLength = [_attrStr length]; unichar lastChar = (textLength > 0) ? [[_attrStr string] characterAtIndex:textLength - 1] : '\n'; NSRange rangeToReplace = (suppressTrailingSpace && _flags.isSoft && (lastChar == ' ' || lastChar == NSLineSeparatorCharacter)) ? NSMakeRange(textLength - 1, 1) : NSMakeRange(textLength, 0); BOOL needBreak = (flag || lastChar != '\n'); if (needBreak) { NSString *string = (([@"BODY" isEqualToString:tag] || [@"HTML" isEqualToString:tag]) ? @"" : @"\n"); [_writingDirectionArray removeAllObjects]; [_attrStr replaceCharactersInRange:rangeToReplace withString:string]; if (rangeToReplace.location < _domRangeStartIndex) _domRangeStartIndex += [string length] - rangeToReplace.length; rangeToReplace.length = [string length]; NSDictionary *attrs = attributesForElement(element); if (rangeToReplace.length > 0) [_attrStr setAttributes:attrs range:rangeToReplace]; _flags.isSoft = YES; } } void HTMLConverter::_newLineForElement(Element& element) { unichar c = NSLineSeparatorCharacter; RetainPtr<NSString> string = adoptNS([[NSString alloc] initWithCharacters:&c length:1]); NSUInteger textLength = [_attrStr length]; NSRange rangeToReplace = NSMakeRange(textLength, 0); [_attrStr replaceCharactersInRange:rangeToReplace withString:string.get()]; rangeToReplace.length = [string length]; if (rangeToReplace.location < _domRangeStartIndex) _domRangeStartIndex += rangeToReplace.length; NSDictionary *attrs = attributesForElement(element); if (rangeToReplace.length > 0) [_attrStr setAttributes:attrs range:rangeToReplace]; _flags.isSoft = YES; } void HTMLConverter::_newTabForElement(Element& element) { NSString *string = @"\t"; NSUInteger textLength = [_attrStr length]; unichar lastChar = (textLength > 0) ? [[_attrStr string] characterAtIndex:textLength - 1] : '\n'; NSRange rangeToReplace = (_flags.isSoft && lastChar == ' ') ? NSMakeRange(textLength - 1, 1) : NSMakeRange(textLength, 0); [_attrStr replaceCharactersInRange:rangeToReplace withString:string]; rangeToReplace.length = [string length]; if (rangeToReplace.location < _domRangeStartIndex) _domRangeStartIndex += rangeToReplace.length; NSDictionary *attrs = attributesForElement(element); if (rangeToReplace.length > 0) [_attrStr setAttributes:attrs range:rangeToReplace]; _flags.isSoft = YES; } static Class _WebMessageDocumentClass() { static Class _WebMessageDocumentClass = Nil; static BOOL lookedUpClass = NO; if (!lookedUpClass) { // If the class is not there, we don't want to try again #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101400 _WebMessageDocumentClass = objc_lookUpClass("EditableWebMessageDocument"); #endif if (!_WebMessageDocumentClass) _WebMessageDocumentClass = objc_lookUpClass("WebMessageDocument"); if (_WebMessageDocumentClass && ![_WebMessageDocumentClass respondsToSelector:@selector(document:attachment:forURL:)]) _WebMessageDocumentClass = Nil; lookedUpClass = YES; } return _WebMessageDocumentClass; } BOOL HTMLConverter::_addAttachmentForElement(Element& element, NSURL *url, BOOL needsParagraph, BOOL usePlaceholder) { BOOL retval = NO; BOOL notFound = NO; NSFileWrapper *fileWrapper = nil; Frame* frame = element.document().frame(); DocumentLoader *dataSource = frame->loader().frameHasLoaded() ? frame->loader().documentLoader() : 0; BOOL ignoreOrientation = YES; if ([url isFileURL]) { NSString *path = [[url path] stringByStandardizingPath]; if (path) fileWrapper = [[[NSFileWrapper alloc] initWithURL:url options:0 error:NULL] autorelease]; } if (!fileWrapper && dataSource) { RefPtr<ArchiveResource> resource = dataSource->subresource(url); if (!resource) resource = dataSource->subresource(url); const String& mimeType = resource->mimeType(); if (usePlaceholder && resource && mimeType == "text/html") notFound = YES; if (resource && !notFound) { fileWrapper = [[[NSFileWrapper alloc] initRegularFileWithContents:resource->data().createNSData().get()] autorelease]; [fileWrapper setPreferredFilename:suggestedFilenameWithMIMEType(url, mimeType)]; } } #if !PLATFORM(IOS_FAMILY) if (!fileWrapper && !notFound) { fileWrapper = fileWrapperForURL(dataSource, url); if (usePlaceholder && fileWrapper && [[[[fileWrapper preferredFilename] pathExtension] lowercaseString] hasPrefix:@"htm"]) notFound = YES; if (notFound) fileWrapper = nil; } if (!fileWrapper && !notFound) { fileWrapper = fileWrapperForURL(m_dataSource, url); if (usePlaceholder && fileWrapper && [[[[fileWrapper preferredFilename] pathExtension] lowercaseString] hasPrefix:@"htm"]) notFound = YES; if (notFound) fileWrapper = nil; } #endif if (!fileWrapper && !notFound && url) { // Special handling for Mail attachments, until WebKit provides a standard way to get the data. Class WebMessageDocumentClass = _WebMessageDocumentClass(); if (WebMessageDocumentClass) { NSTextAttachment *mimeTextAttachment = nil; [WebMessageDocumentClass document:NULL attachment:&mimeTextAttachment forURL:url]; if (mimeTextAttachment && [mimeTextAttachment respondsToSelector:@selector(fileWrapper)]) { fileWrapper = [mimeTextAttachment performSelector:@selector(fileWrapper)]; ignoreOrientation = NO; } } } if (fileWrapper || usePlaceholder) { NSUInteger textLength = [_attrStr length]; RetainPtr<NSTextAttachment> attachment = adoptNS([[PlatformNSTextAttachment alloc] initWithFileWrapper:fileWrapper]); #if PLATFORM(IOS_FAMILY) float verticalAlign = 0.0; _caches->floatPropertyValueForNode(element, CSSPropertyVerticalAlign, verticalAlign); attachment.get().bounds = CGRectMake(0, (verticalAlign / 100) * element.clientHeight(), element.clientWidth(), element.clientHeight()); #endif RetainPtr<NSString> string = adoptNS([[NSString alloc] initWithFormat:(needsParagraph ? @"%C\n" : @"%C"), static_cast<unichar>(NSAttachmentCharacter)]); NSRange rangeToReplace = NSMakeRange(textLength, 0); NSDictionary *attrs; if (fileWrapper) { #if !PLATFORM(IOS_FAMILY) if (ignoreOrientation) [attachment setIgnoresOrientation:YES]; #endif } else { NSBundle *webCoreBundle = [NSBundle bundleWithIdentifier:@"com.apple.WebCore"]; #if PLATFORM(IOS_FAMILY) UIImage *missingImage = [PlatformImageClass imageNamed:@"missingImage" inBundle:webCoreBundle compatibleWithTraitCollection:nil]; #else NSImage *missingImage = [webCoreBundle imageForResource:@"missingImage"]; #endif ASSERT_WITH_MESSAGE(missingImage != nil, "Unable to find missingImage."); attachment = adoptNS([[PlatformNSTextAttachment alloc] initWithData:nil ofType:nil]); attachment.get().image = missingImage; } [_attrStr replaceCharactersInRange:rangeToReplace withString:string.get()]; rangeToReplace.length = [string length]; if (rangeToReplace.location < _domRangeStartIndex) _domRangeStartIndex += rangeToReplace.length; attrs = attributesForElement(element); if (rangeToReplace.length > 0) { [_attrStr setAttributes:attrs range:rangeToReplace]; rangeToReplace.length = 1; [_attrStr addAttribute:NSAttachmentAttributeName value:attachment.get() range:rangeToReplace]; } _flags.isSoft = NO; retval = YES; } return retval; } void HTMLConverter::_addQuoteForElement(Element& element, BOOL opening, NSInteger level) { unichar c = ((level % 2) == 0) ? (opening ? 0x201c : 0x201d) : (opening ? 0x2018 : 0x2019); RetainPtr<NSString> string = adoptNS([[NSString alloc] initWithCharacters:&c length:1]); NSUInteger textLength = [_attrStr length]; NSRange rangeToReplace = NSMakeRange(textLength, 0); [_attrStr replaceCharactersInRange:rangeToReplace withString:string.get()]; rangeToReplace.length = [string length]; if (rangeToReplace.location < _domRangeStartIndex) _domRangeStartIndex += rangeToReplace.length; RetainPtr<NSDictionary> attrs = attributesForElement(element); if (rangeToReplace.length > 0) [_attrStr setAttributes:attrs.get() range:rangeToReplace]; _flags.isSoft = NO; } void HTMLConverter::_addValue(NSString *value, Element& element) { NSUInteger textLength = [_attrStr length]; NSUInteger valueLength = [value length]; NSRange rangeToReplace = NSMakeRange(textLength, 0); if (valueLength) { [_attrStr replaceCharactersInRange:rangeToReplace withString:value]; rangeToReplace.length = valueLength; if (rangeToReplace.location < _domRangeStartIndex) _domRangeStartIndex += rangeToReplace.length; RetainPtr<NSDictionary> attrs = attributesForElement(element); if (rangeToReplace.length > 0) [_attrStr setAttributes:attrs.get() range:rangeToReplace]; _flags.isSoft = NO; } } void HTMLConverter::_fillInBlock(NSTextBlock *block, Element& element, PlatformColor *backgroundColor, CGFloat extraMargin, CGFloat extraPadding, BOOL isTable) { float result = 0; NSString *width = element.getAttribute(widthAttr); if ((width && [width length]) || !isTable) { if (_caches->floatPropertyValueForNode(element, CSSPropertyWidth, result)) [block setValue:result type:NSTextBlockAbsoluteValueType forDimension:NSTextBlockWidth]; } if (_caches->floatPropertyValueForNode(element, CSSPropertyMinWidth, result)) [block setValue:result type:NSTextBlockAbsoluteValueType forDimension:NSTextBlockMinimumWidth]; if (_caches->floatPropertyValueForNode(element, CSSPropertyMaxWidth, result)) [block setValue:result type:NSTextBlockAbsoluteValueType forDimension:NSTextBlockMaximumWidth]; if (_caches->floatPropertyValueForNode(element, CSSPropertyMinHeight, result)) [block setValue:result type:NSTextBlockAbsoluteValueType forDimension:NSTextBlockMinimumHeight]; if (_caches->floatPropertyValueForNode(element, CSSPropertyMaxHeight, result)) [block setValue:result type:NSTextBlockAbsoluteValueType forDimension:NSTextBlockMaximumHeight]; if (_caches->floatPropertyValueForNode(element, CSSPropertyPaddingLeft, result)) [block setWidth:result + extraPadding type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding edge:NSMinXEdge]; else [block setWidth:extraPadding type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding edge:NSMinXEdge]; if (_caches->floatPropertyValueForNode(element, CSSPropertyPaddingTop, result)) [block setWidth:result + extraPadding type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding edge:NSMinYEdge]; else [block setWidth:extraPadding type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding edge:NSMinYEdge]; if (_caches->floatPropertyValueForNode(element, CSSPropertyPaddingRight, result)) [block setWidth:result + extraPadding type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding edge:NSMaxXEdge]; else [block setWidth:extraPadding type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding edge:NSMaxXEdge]; if (_caches->floatPropertyValueForNode(element, CSSPropertyPaddingBottom, result)) [block setWidth:result + extraPadding type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding edge:NSMaxYEdge]; else [block setWidth:extraPadding type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding edge:NSMaxYEdge]; if (_caches->floatPropertyValueForNode(element, CSSPropertyBorderLeftWidth, result)) [block setWidth:result type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockBorder edge:NSMinXEdge]; if (_caches->floatPropertyValueForNode(element, CSSPropertyBorderTopWidth, result)) [block setWidth:result type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockBorder edge:NSMinYEdge]; if (_caches->floatPropertyValueForNode(element, CSSPropertyBorderRightWidth, result)) [block setWidth:result type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockBorder edge:NSMaxXEdge]; if (_caches->floatPropertyValueForNode(element, CSSPropertyBorderBottomWidth, result)) [block setWidth:result type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockBorder edge:NSMaxYEdge]; if (_caches->floatPropertyValueForNode(element, CSSPropertyMarginLeft, result)) [block setWidth:result + extraMargin type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockMargin edge:NSMinXEdge]; else [block setWidth:extraMargin type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockMargin edge:NSMinXEdge]; if (_caches->floatPropertyValueForNode(element, CSSPropertyMarginTop, result)) [block setWidth:result + extraMargin type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockMargin edge:NSMinYEdge]; else [block setWidth:extraMargin type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockMargin edge:NSMinYEdge]; if (_caches->floatPropertyValueForNode(element, CSSPropertyMarginRight, result)) [block setWidth:result + extraMargin type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockMargin edge:NSMaxXEdge]; else [block setWidth:extraMargin type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockMargin edge:NSMaxXEdge]; if (_caches->floatPropertyValueForNode(element, CSSPropertyMarginBottom, result)) [block setWidth:result + extraMargin type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockMargin edge:NSMaxYEdge]; else [block setWidth:extraMargin type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockMargin edge:NSMaxYEdge]; PlatformColor *color = nil; if ((color = _colorForElement(element, CSSPropertyBackgroundColor))) [block setBackgroundColor:color]; if (!color && backgroundColor) [block setBackgroundColor:backgroundColor]; if ((color = _colorForElement(element, CSSPropertyBorderLeftColor))) [block setBorderColor:color forEdge:NSMinXEdge]; if ((color = _colorForElement(element, CSSPropertyBorderTopColor))) [block setBorderColor:color forEdge:NSMinYEdge]; if ((color = _colorForElement(element, CSSPropertyBorderRightColor))) [block setBorderColor:color forEdge:NSMaxXEdge]; if ((color = _colorForElement(element, CSSPropertyBorderBottomColor))) [block setBorderColor:color forEdge:NSMaxYEdge]; } static inline BOOL read2DigitNumber(const char **pp, int8_t *outval) { BOOL result = NO; char c1 = *(*pp)++, c2; if (isASCIIDigit(c1)) { c2 = *(*pp)++; if (isASCIIDigit(c2)) { *outval = 10 * (c1 - '0') + (c2 - '0'); result = YES; } } return result; } static inline NSDate *_dateForString(NSString *string) { const char *p = [string UTF8String]; RetainPtr<NSDateComponents> dateComponents = adoptNS([[NSDateComponents alloc] init]); // Set the time zone to GMT [dateComponents setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; NSInteger year = 0; while (*p && isASCIIDigit(*p)) year = 10 * year + *p++ - '0'; if (*p++ != '-') return nil; [dateComponents setYear:year]; int8_t component; if (!read2DigitNumber(&p, &component) || *p++ != '-') return nil; [dateComponents setMonth:component]; if (!read2DigitNumber(&p, &component) || *p++ != 'T') return nil; [dateComponents setDay:component]; if (!read2DigitNumber(&p, &component) || *p++ != ':') return nil; [dateComponents setHour:component]; if (!read2DigitNumber(&p, &component) || *p++ != ':') return nil; [dateComponents setMinute:component]; if (!read2DigitNumber(&p, &component) || *p++ != 'Z') return nil; [dateComponents setSecond:component]; return [[[[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian] autorelease] dateFromComponents:dateComponents.get()]; } static NSInteger _colCompare(id block1, id block2, void *) { NSInteger col1 = [(NSTextTableBlock *)block1 startingColumn]; NSInteger col2 = [(NSTextTableBlock *)block2 startingColumn]; return ((col1 < col2) ? NSOrderedAscending : ((col1 == col2) ? NSOrderedSame : NSOrderedDescending)); } void HTMLConverter::_processMetaElementWithName(NSString *name, NSString *content) { NSString *key = nil; if (NSOrderedSame == [@"CocoaVersion" compare:name options:NSCaseInsensitiveSearch]) { CGFloat versionNumber = [content doubleValue]; if (versionNumber > 0.0) { // ??? this should be keyed off of version number in future [_documentAttrs removeObjectForKey:NSConvertedDocumentAttribute]; [_documentAttrs setObject:[NSNumber numberWithDouble:versionNumber] forKey:NSCocoaVersionDocumentAttribute]; } #if PLATFORM(IOS_FAMILY) } else if (NSOrderedSame == [@"Generator" compare:name options:NSCaseInsensitiveSearch]) { key = NSGeneratorDocumentAttribute; #endif } else if (NSOrderedSame == [@"Keywords" compare:name options:NSCaseInsensitiveSearch]) { if (content && [content length] > 0) { NSArray *array; // ??? need better handling here and throughout if ([content rangeOfString:@", "].length == 0 && [content rangeOfString:@","].length > 0) array = [content componentsSeparatedByString:@","]; else if ([content rangeOfString:@", "].length == 0 && [content rangeOfString:@" "].length > 0) array = [content componentsSeparatedByString:@" "]; else array = [content componentsSeparatedByString:@", "]; [_documentAttrs setObject:array forKey:NSKeywordsDocumentAttribute]; } } else if (NSOrderedSame == [@"Author" compare:name options:NSCaseInsensitiveSearch]) key = NSAuthorDocumentAttribute; else if (NSOrderedSame == [@"LastAuthor" compare:name options:NSCaseInsensitiveSearch]) key = NSEditorDocumentAttribute; else if (NSOrderedSame == [@"Company" compare:name options:NSCaseInsensitiveSearch]) key = NSCompanyDocumentAttribute; else if (NSOrderedSame == [@"Copyright" compare:name options:NSCaseInsensitiveSearch]) key = NSCopyrightDocumentAttribute; else if (NSOrderedSame == [@"Subject" compare:name options:NSCaseInsensitiveSearch]) key = NSSubjectDocumentAttribute; else if (NSOrderedSame == [@"Description" compare:name options:NSCaseInsensitiveSearch] || NSOrderedSame == [@"Comment" compare:name options:NSCaseInsensitiveSearch]) key = NSCommentDocumentAttribute; else if (NSOrderedSame == [@"CreationTime" compare:name options:NSCaseInsensitiveSearch]) { if (content && [content length] > 0) { NSDate *date = _dateForString(content); if (date) [_documentAttrs setObject:date forKey:NSCreationTimeDocumentAttribute]; } } else if (NSOrderedSame == [@"ModificationTime" compare:name options:NSCaseInsensitiveSearch]) { if (content && [content length] > 0) { NSDate *date = _dateForString(content); if (date) [_documentAttrs setObject:date forKey:NSModificationTimeDocumentAttribute]; } } #if PLATFORM(IOS_FAMILY) else if (NSOrderedSame == [@"DisplayName" compare:name options:NSCaseInsensitiveSearch] || NSOrderedSame == [@"IndexTitle" compare:name options:NSCaseInsensitiveSearch]) key = NSDisplayNameDocumentAttribute; else if (NSOrderedSame == [@"robots" compare:name options:NSCaseInsensitiveSearch]) { if ([content rangeOfString:@"noindex" options:NSCaseInsensitiveSearch].length > 0) [_documentAttrs setObject:[NSNumber numberWithInteger:1] forKey:NSNoIndexDocumentAttribute]; } #endif if (key && content && [content length] > 0) [_documentAttrs setObject:content forKey:key]; } void HTMLConverter::_processHeadElement(Element& element) { // FIXME: Should gather data from other sources e.g. Word, but for that we would need to be able to get comments from DOM for (HTMLMetaElement* child = Traversal<HTMLMetaElement>::firstChild(element); child; child = Traversal<HTMLMetaElement>::nextSibling(*child)) { NSString *name = child->name(); NSString *content = child->content(); if (name && content) _processMetaElementWithName(name, content); } } BOOL HTMLConverter::_enterElement(Element& element, BOOL embedded) { String displayValue = _caches->propertyValueForNode(element, CSSPropertyDisplay); if (element.hasTagName(headTag) && !embedded) _processHeadElement(element); else if (!displayValue.length() || !(displayValue == "none" || displayValue == "table-column" || displayValue == "table-column-group")) { if (_caches->isBlockElement(element) && !element.hasTagName(brTag) && !(displayValue == "table-cell" && [_textTables count] == 0) && !([_textLists count] > 0 && displayValue == "block" && !element.hasTagName(liTag) && !element.hasTagName(ulTag) && !element.hasTagName(olTag))) _newParagraphForElement(element, element.tagName(), NO, YES); return YES; } return NO; } void HTMLConverter::_addTableForElement(Element *tableElement) { RetainPtr<NSTextTable> table = adoptNS([(NSTextTable *)[PlatformNSTextTable alloc] init]); CGFloat cellSpacingVal = 1; CGFloat cellPaddingVal = 1; [table setNumberOfColumns:1]; [table setLayoutAlgorithm:NSTextTableAutomaticLayoutAlgorithm]; [table setCollapsesBorders:NO]; [table setHidesEmptyCells:NO]; if (tableElement) { ASSERT(tableElement); Element& coreTableElement = *tableElement; NSString *cellSpacing = coreTableElement.getAttribute(cellspacingAttr); if (cellSpacing && [cellSpacing length] > 0 && ![cellSpacing hasSuffix:@"%"]) cellSpacingVal = [cellSpacing floatValue]; NSString *cellPadding = coreTableElement.getAttribute(cellpaddingAttr); if (cellPadding && [cellPadding length] > 0 && ![cellPadding hasSuffix:@"%"]) cellPaddingVal = [cellPadding floatValue]; _fillInBlock(table.get(), coreTableElement, nil, 0, 0, YES); if (_caches->propertyValueForNode(coreTableElement, CSSPropertyBorderCollapse) == "collapse") { [table setCollapsesBorders:YES]; cellSpacingVal = 0; } if (_caches->propertyValueForNode(coreTableElement, CSSPropertyEmptyCells) == "hide") [table setHidesEmptyCells:YES]; if (_caches->propertyValueForNode(coreTableElement, CSSPropertyTableLayout) == "fixed") [table setLayoutAlgorithm:NSTextTableFixedLayoutAlgorithm]; } [_textTables addObject:table.get()]; [_textTableSpacings addObject:[NSNumber numberWithDouble:cellSpacingVal]]; [_textTablePaddings addObject:[NSNumber numberWithDouble:cellPaddingVal]]; [_textTableRows addObject:[NSNumber numberWithInteger:0]]; [_textTableRowArrays addObject:[NSMutableArray array]]; } void HTMLConverter::_addTableCellForElement(Element* element) { NSTextTable *table = [_textTables lastObject]; NSInteger rowNumber = [[_textTableRows lastObject] integerValue]; NSInteger columnNumber = 0; NSInteger rowSpan = 1; NSInteger colSpan = 1; NSMutableArray *rowArray = [_textTableRowArrays lastObject]; NSUInteger count = [rowArray count]; PlatformColor *color = ([_textTableRowBackgroundColors count] > 0) ? [_textTableRowBackgroundColors lastObject] : nil; NSTextTableBlock *previousBlock; CGFloat cellSpacingVal = [[_textTableSpacings lastObject] floatValue]; if ([color isEqual:[PlatformColorClass clearColor]]) color = nil; for (NSUInteger i = 0; i < count; i++) { previousBlock = [rowArray objectAtIndex:i]; if (columnNumber >= [previousBlock startingColumn] && columnNumber < [previousBlock startingColumn] + [previousBlock columnSpan]) columnNumber = [previousBlock startingColumn] + [previousBlock columnSpan]; } RetainPtr<NSTextTableBlock> block; if (element) { if (is<HTMLTableCellElement>(*element)) { HTMLTableCellElement& tableCellElement = downcast<HTMLTableCellElement>(*element); rowSpan = tableCellElement.rowSpan(); if (rowSpan < 1) rowSpan = 1; colSpan = tableCellElement.colSpan(); if (colSpan < 1) colSpan = 1; } block = adoptNS([[PlatformNSTextTableBlock alloc] initWithTable:table startingRow:rowNumber rowSpan:rowSpan startingColumn:columnNumber columnSpan:colSpan]); String verticalAlign = _caches->propertyValueForNode(*element, CSSPropertyVerticalAlign); _fillInBlock(block.get(), *element, color, cellSpacingVal / 2, 0, NO); if (verticalAlign == "middle") [block setVerticalAlignment:NSTextBlockMiddleAlignment]; else if (verticalAlign == "bottom") [block setVerticalAlignment:NSTextBlockBottomAlignment]; else if (verticalAlign == "baseline") [block setVerticalAlignment:NSTextBlockBaselineAlignment]; else if (verticalAlign == "top") [block setVerticalAlignment:NSTextBlockTopAlignment]; } else { block = adoptNS([[PlatformNSTextTableBlock alloc] initWithTable:table startingRow:rowNumber rowSpan:rowSpan startingColumn:columnNumber columnSpan:colSpan]); } [_textBlocks addObject:block.get()]; [rowArray addObject:block.get()]; [rowArray sortUsingFunction:_colCompare context:NULL]; } BOOL HTMLConverter::_processElement(Element& element, NSInteger depth) { BOOL retval = YES; BOOL isBlockLevel = _caches->isBlockElement(element); String displayValue = _caches->propertyValueForNode(element, CSSPropertyDisplay); if (isBlockLevel) [_writingDirectionArray removeAllObjects]; else { String bidi = _caches->propertyValueForNode(element, CSSPropertyUnicodeBidi); if (bidi == "embed") { NSUInteger val = NSWritingDirectionEmbedding; if (_caches->propertyValueForNode(element, CSSPropertyDirection) == "rtl") val |= NSWritingDirectionRightToLeft; [_writingDirectionArray addObject:[NSNumber numberWithUnsignedInteger:val]]; } else if (bidi == "bidi-override") { NSUInteger val = NSWritingDirectionOverride; if (_caches->propertyValueForNode(element, CSSPropertyDirection) == "rtl") val |= NSWritingDirectionRightToLeft; [_writingDirectionArray addObject:[NSNumber numberWithUnsignedInteger:val]]; } } if (displayValue == "table" || ([_textTables count] == 0 && displayValue == "table-row-group")) { Element* tableElement = &element; if (displayValue == "table-row-group") { // If we are starting in medias res, the first thing we see may be the tbody, so go up to the table tableElement = _blockLevelElementForNode(element.parentInComposedTree()); if (!tableElement || _caches->propertyValueForNode(*tableElement, CSSPropertyDisplay) != "table") tableElement = &element; } while ([_textTables count] > [_textBlocks count]) _addTableCellForElement(nil); _addTableForElement(tableElement); } else if (displayValue == "table-footer-group" && [_textTables count] > 0) { m_textTableFooters.add((__bridge CFTypeRef)[_textTables lastObject], &element); retval = NO; } else if (displayValue == "table-row" && [_textTables count] > 0) { PlatformColor *color = _colorForElement(element, CSSPropertyBackgroundColor); if (!color) color = (PlatformColor *)[PlatformColorClass clearColor]; [_textTableRowBackgroundColors addObject:color]; } else if (displayValue == "table-cell") { while ([_textTables count] < [_textBlocks count] + 1) _addTableForElement(nil); _addTableCellForElement(&element); #if ENABLE(ATTACHMENT_ELEMENT) } else if (is<HTMLAttachmentElement>(element)) { HTMLAttachmentElement& attachment = downcast<HTMLAttachmentElement>(element); if (attachment.file()) { NSURL *url = [NSURL fileURLWithPath:attachment.file()->path()]; if (url) _addAttachmentForElement(element, url, isBlockLevel, NO); } retval = NO; #endif } else if (element.hasTagName(imgTag)) { NSString *urlString = element.imageSourceURL(); if (urlString && [urlString length] > 0) { NSURL *url = element.document().completeURL(stripLeadingAndTrailingHTMLSpaces(urlString)); if (!url) url = [NSURL _web_URLWithString:[urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] relativeToURL:_baseURL]; #if PLATFORM(IOS_FAMILY) BOOL usePlaceholderImage = NO; #else BOOL usePlaceholderImage = YES; #endif if (url) _addAttachmentForElement(element, url, isBlockLevel, usePlaceholderImage); } retval = NO; } else if (element.hasTagName(objectTag)) { NSString *baseString = element.getAttribute(codebaseAttr); NSString *urlString = element.getAttribute(dataAttr); NSString *declareString = element.getAttribute(declareAttr); if (urlString && [urlString length] > 0 && ![@"true" isEqualToString:declareString]) { NSURL *baseURL = nil; NSURL *url = nil; if (baseString && [baseString length] > 0) { baseURL = element.document().completeURL(stripLeadingAndTrailingHTMLSpaces(baseString)); if (!baseURL) baseURL = [NSURL _web_URLWithString:[baseString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] relativeToURL:_baseURL]; } if (baseURL) url = [NSURL _web_URLWithString:[urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] relativeToURL:baseURL]; if (!url) url = element.document().completeURL(stripLeadingAndTrailingHTMLSpaces(urlString)); if (!url) url = [NSURL _web_URLWithString:[urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] relativeToURL:_baseURL]; if (url) retval = !_addAttachmentForElement(element, url, isBlockLevel, NO); } } else if (is<HTMLFrameElementBase>(element)) { if (Document* contentDocument = downcast<HTMLFrameElementBase>(element).contentDocument()) { _traverseNode(*contentDocument, depth + 1, true /* embedded */); retval = NO; } } else if (element.hasTagName(brTag)) { Element* blockElement = _blockLevelElementForNode(element.parentInComposedTree()); NSString *breakClass = element.getAttribute(classAttr); NSString *blockTag = blockElement ? (NSString *)blockElement->tagName() : nil; BOOL isExtraBreak = [@"Apple-interchange-newline" isEqualToString:breakClass]; BOOL blockElementIsParagraph = ([@"P" isEqualToString:blockTag] || [@"LI" isEqualToString:blockTag] || ([blockTag hasPrefix:@"H"] && 2 == [blockTag length])); if (isExtraBreak) _flags.hasTrailingNewline = YES; else { if (blockElement && blockElementIsParagraph) _newLineForElement(element); else _newParagraphForElement(element, element.tagName(), YES, NO); } } else if (element.hasTagName(ulTag)) { RetainPtr<NSTextList> list; String listStyleType = _caches->propertyValueForNode(element, CSSPropertyListStyleType); if (!listStyleType.length()) listStyleType = @"disc"; list = adoptNS([[PlatformNSTextList alloc] initWithMarkerFormat:String("{" + listStyleType + "}") options:0]); [_textLists addObject:list.get()]; } else if (element.hasTagName(olTag)) { RetainPtr<NSTextList> list; String listStyleType = _caches->propertyValueForNode(element, CSSPropertyListStyleType); if (!listStyleType.length()) listStyleType = "decimal"; list = adoptNS([[PlatformNSTextList alloc] initWithMarkerFormat:String("{" + listStyleType + "}") options:0]); if (is<HTMLOListElement>(element)) { NSInteger startingItemNumber = downcast<HTMLOListElement>(element).start(); [list setStartingItemNumber:startingItemNumber]; } [_textLists addObject:list.get()]; } else if (element.hasTagName(qTag)) { _addQuoteForElement(element, YES, _quoteLevel++); } else if (element.hasTagName(inputTag)) { if (is<HTMLInputElement>(element)) { HTMLInputElement& inputElement = downcast<HTMLInputElement>(element); if (inputElement.type() == "text") { NSString *value = inputElement.value(); if (value && [value length] > 0) _addValue(value, element); } } } else if (element.hasTagName(textareaTag)) { if (is<HTMLTextAreaElement>(element)) { HTMLTextAreaElement& textAreaElement = downcast<HTMLTextAreaElement>(element); NSString *value = textAreaElement.value(); if (value && [value length] > 0) _addValue(value, element); } retval = NO; } return retval; } void HTMLConverter::_addMarkersToList(NSTextList *list, NSRange range) { NSInteger itemNum = [list startingItemNumber]; NSString *string = [_attrStr string]; NSString *stringToInsert; NSDictionary *attrsToInsert = nil; PlatformFont *font; NSParagraphStyle *paragraphStyle; NSMutableParagraphStyle *newStyle; NSTextTab *tab = nil; NSTextTab *tabToRemove; NSRange paragraphRange; NSRange styleRange; NSUInteger textLength = [_attrStr length]; NSUInteger listIndex; NSUInteger insertLength; NSUInteger i; NSUInteger count; NSArray *textLists; CGFloat markerLocation; CGFloat listLocation; if (range.length == 0 || range.location >= textLength) return; if (NSMaxRange(range) > textLength) range.length = textLength - range.location; paragraphStyle = [_attrStr attribute:NSParagraphStyleAttributeName atIndex:range.location effectiveRange:NULL]; if (paragraphStyle) { textLists = [paragraphStyle textLists]; listIndex = [textLists indexOfObject:list]; if (textLists && listIndex != NSNotFound) { for (NSUInteger idx = range.location; idx < NSMaxRange(range);) { paragraphRange = [string paragraphRangeForRange:NSMakeRange(idx, 0)]; paragraphStyle = [_attrStr attribute:NSParagraphStyleAttributeName atIndex:idx effectiveRange:&styleRange]; font = [_attrStr attribute:NSFontAttributeName atIndex:idx effectiveRange:NULL]; if ([[paragraphStyle textLists] count] == listIndex + 1) { stringToInsert = [NSString stringWithFormat:@"\t%@\t", [list markerForItemNumber:itemNum++]]; insertLength = [stringToInsert length]; attrsToInsert = [PlatformNSTextList _standardMarkerAttributesForAttributes:[_attrStr attributesAtIndex:paragraphRange.location effectiveRange:NULL]]; [_attrStr replaceCharactersInRange:NSMakeRange(paragraphRange.location, 0) withString:stringToInsert]; [_attrStr setAttributes:attrsToInsert range:NSMakeRange(paragraphRange.location, insertLength)]; range.length += insertLength; paragraphRange.length += insertLength; if (paragraphRange.location < _domRangeStartIndex) _domRangeStartIndex += insertLength; newStyle = [paragraphStyle mutableCopy]; listLocation = (listIndex + 1) * 36; markerLocation = listLocation - 25; [newStyle setFirstLineHeadIndent:0]; [newStyle setHeadIndent:listLocation]; while ((count = [[newStyle tabStops] count]) > 0) { for (i = 0, tabToRemove = nil; !tabToRemove && i < count; i++) { tab = [[newStyle tabStops] objectAtIndex:i]; if ([tab location] <= listLocation) tabToRemove = tab; } if (tabToRemove) [newStyle removeTabStop:tab]; else break; } tab = [[PlatformNSTextTab alloc] initWithType:NSLeftTabStopType location:markerLocation]; [newStyle addTabStop:tab]; [tab release]; tab = [[PlatformNSTextTab alloc] initWithTextAlignment:NSTextAlignmentNatural location:listLocation options:@{ }]; [newStyle addTabStop:tab]; [tab release]; [_attrStr addAttribute:NSParagraphStyleAttributeName value:newStyle range:paragraphRange]; [newStyle release]; idx = NSMaxRange(paragraphRange); } else { // skip any deeper-nested lists idx = NSMaxRange(styleRange); } } } } } void HTMLConverter::_exitElement(Element& element, NSInteger depth, NSUInteger startIndex) { String displayValue = _caches->propertyValueForNode(element, CSSPropertyDisplay); NSRange range = NSMakeRange(startIndex, [_attrStr length] - startIndex); if (range.length > 0 && element.hasTagName(aTag)) { NSString *urlString = element.getAttribute(hrefAttr); NSString *strippedString = [urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (urlString && [urlString length] > 0 && strippedString && [strippedString length] > 0 && ![strippedString hasPrefix:@"#"]) { NSURL *url = element.document().completeURL(stripLeadingAndTrailingHTMLSpaces(urlString)); if (!url) url = element.document().completeURL(stripLeadingAndTrailingHTMLSpaces(strippedString)); if (!url) url = [NSURL _web_URLWithString:strippedString relativeToURL:_baseURL]; [_attrStr addAttribute:NSLinkAttributeName value:url ? (id)url : (id)urlString range:range]; } } if (!_flags.reachedEnd && _caches->isBlockElement(element)) { [_writingDirectionArray removeAllObjects]; if (displayValue == "table-cell" && [_textBlocks count] == 0) { _newTabForElement(element); } else if ([_textLists count] > 0 && displayValue == "block" && !element.hasTagName(liTag) && !element.hasTagName(ulTag) && !element.hasTagName(olTag)) { _newLineForElement(element); } else { _newParagraphForElement(element, element.tagName(), (range.length == 0), YES); } } else if ([_writingDirectionArray count] > 0) { String bidi = _caches->propertyValueForNode(element, CSSPropertyUnicodeBidi); if (bidi == "embed" || bidi == "bidi-override") [_writingDirectionArray removeLastObject]; } range = NSMakeRange(startIndex, [_attrStr length] - startIndex); if (displayValue == "table" && [_textTables count] > 0) { NSTextTable *key = [_textTables lastObject]; Element* footer = m_textTableFooters.get((__bridge CFTypeRef)key); while ([_textTables count] < [_textBlocks count] + 1) [_textBlocks removeLastObject]; if (footer) { _traverseFooterNode(*footer, depth + 1); m_textTableFooters.remove((__bridge CFTypeRef)key); } [_textTables removeLastObject]; [_textTableSpacings removeLastObject]; [_textTablePaddings removeLastObject]; [_textTableRows removeLastObject]; [_textTableRowArrays removeLastObject]; } else if (displayValue == "table-row" && [_textTables count] > 0) { NSTextTable *table = [_textTables lastObject]; NSTextTableBlock *block; NSMutableArray *rowArray = [_textTableRowArrays lastObject], *previousRowArray; NSUInteger i, count; NSInteger numberOfColumns = [table numberOfColumns]; NSInteger openColumn; NSInteger rowNumber = [[_textTableRows lastObject] integerValue]; do { rowNumber++; previousRowArray = rowArray; rowArray = [NSMutableArray array]; count = [previousRowArray count]; for (i = 0; i < count; i++) { block = [previousRowArray objectAtIndex:i]; if ([block startingColumn] + [block columnSpan] > numberOfColumns) numberOfColumns = [block startingColumn] + [block columnSpan]; if ([block startingRow] + [block rowSpan] > rowNumber) [rowArray addObject:block]; } count = [rowArray count]; openColumn = 0; for (i = 0; i < count; i++) { block = [rowArray objectAtIndex:i]; if (openColumn >= [block startingColumn] && openColumn < [block startingColumn] + [block columnSpan]) openColumn = [block startingColumn] + [block columnSpan]; } } while (openColumn >= numberOfColumns); if ((NSUInteger)numberOfColumns > [table numberOfColumns]) [table setNumberOfColumns:numberOfColumns]; [_textTableRows removeLastObject]; [_textTableRows addObject:[NSNumber numberWithInteger:rowNumber]]; [_textTableRowArrays removeLastObject]; [_textTableRowArrays addObject:rowArray]; if ([_textTableRowBackgroundColors count] > 0) [_textTableRowBackgroundColors removeLastObject]; } else if (displayValue == "table-cell" && [_textBlocks count] > 0) { while ([_textTables count] > [_textBlocks count]) { [_textTables removeLastObject]; [_textTableSpacings removeLastObject]; [_textTablePaddings removeLastObject]; [_textTableRows removeLastObject]; [_textTableRowArrays removeLastObject]; } [_textBlocks removeLastObject]; } else if ((element.hasTagName(ulTag) || element.hasTagName(olTag)) && [_textLists count] > 0) { NSTextList *list = [_textLists lastObject]; _addMarkersToList(list, range); [_textLists removeLastObject]; } else if (element.hasTagName(qTag)) { _addQuoteForElement(element, NO, --_quoteLevel); } else if (element.hasTagName(spanTag)) { NSString *className = element.getAttribute(classAttr); NSMutableString *mutableString; NSUInteger i, count = 0; unichar c; if ([@"Apple-converted-space" isEqualToString:className]) { mutableString = [_attrStr mutableString]; for (i = range.location; i < NSMaxRange(range); i++) { c = [mutableString characterAtIndex:i]; if (0xa0 == c) [mutableString replaceCharactersInRange:NSMakeRange(i, 1) withString:@" "]; } } else if ([@"Apple-converted-tab" isEqualToString:className]) { mutableString = [_attrStr mutableString]; for (i = range.location; i < NSMaxRange(range); i++) { NSRange rangeToReplace = NSMakeRange(NSNotFound, 0); c = [mutableString characterAtIndex:i]; if (' ' == c || 0xa0 == c) { count++; if (count >= 4 || i + 1 >= NSMaxRange(range)) rangeToReplace = NSMakeRange(i + 1 - count, count); } else { if (count > 0) rangeToReplace = NSMakeRange(i - count, count); } if (rangeToReplace.length > 0) { [mutableString replaceCharactersInRange:rangeToReplace withString:@"\t"]; range.length -= (rangeToReplace.length - 1); i -= (rangeToReplace.length - 1); if (NSMaxRange(rangeToReplace) <= _domRangeStartIndex) { _domRangeStartIndex -= (rangeToReplace.length - 1); } else if (rangeToReplace.location < _domRangeStartIndex) { _domRangeStartIndex = rangeToReplace.location; } count = 0; } } } } } void HTMLConverter::_processText(CharacterData& characterData) { NSUInteger textLength = [_attrStr length]; unichar lastChar = (textLength > 0) ? [[_attrStr string] characterAtIndex:textLength - 1] : '\n'; BOOL suppressLeadingSpace = ((_flags.isSoft && lastChar == ' ') || lastChar == '\n' || lastChar == '\r' || lastChar == '\t' || lastChar == NSParagraphSeparatorCharacter || lastChar == NSLineSeparatorCharacter || lastChar == NSFormFeedCharacter || lastChar == WebNextLineCharacter); NSRange rangeToReplace = NSMakeRange(textLength, 0); CFMutableStringRef mutstr = NULL; String originalString = characterData.data(); unsigned startOffset = 0; unsigned endOffset = originalString.length(); if (&characterData == m_start.containerNode()) { startOffset = m_start.offsetInContainerNode(); _domRangeStartIndex = [_attrStr length]; _flags.reachedStart = YES; } if (&characterData == m_end.containerNode()) { endOffset = m_end.offsetInContainerNode(); _flags.reachedEnd = YES; } if ((startOffset > 0 || endOffset < originalString.length()) && endOffset >= startOffset) originalString = originalString.substring(startOffset, endOffset - startOffset); String outputString = originalString; // FIXME: Use RenderText's content instead. bool wasSpace = false; if (_caches->propertyValueForNode(characterData, CSSPropertyWhiteSpace).startsWith("pre")) { if (textLength && originalString.length() && _flags.isSoft) { unichar c = originalString.characterAt(0); if (c == '\n' || c == '\r' || c == NSParagraphSeparatorCharacter || c == NSLineSeparatorCharacter || c == NSFormFeedCharacter || c == WebNextLineCharacter) rangeToReplace = NSMakeRange(textLength - 1, 1); } } else { unsigned count = originalString.length(); bool wasLeading = true; StringBuilder builder; LChar noBreakSpaceRepresentation = 0; for (unsigned i = 0; i < count; i++) { UChar c = originalString.characterAt(i); bool isWhitespace = c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == 0xc || c == 0x200b; if (isWhitespace) wasSpace = (!wasLeading || !suppressLeadingSpace); else { if (wasSpace) builder.append(' '); if (c != noBreakSpace) builder.append(c); else { if (!noBreakSpaceRepresentation) noBreakSpaceRepresentation = _caches->propertyValueForNode(characterData, CSSPropertyWebkitNbspMode) == "space" ? ' ' : noBreakSpace; builder.append(noBreakSpaceRepresentation); } wasSpace = false; wasLeading = false; } } if (wasSpace) builder.append(' '); outputString = builder.toString(); } if (outputString.length()) { String textTransform = _caches->propertyValueForNode(characterData, CSSPropertyTextTransform); if (textTransform == "capitalize") outputString = capitalize(outputString, ' '); // FIXME: Needs to take locale into account to work correctly. else if (textTransform == "uppercase") outputString = outputString.convertToUppercaseWithoutLocale(); // FIXME: Needs locale to work correctly. else if (textTransform == "lowercase") outputString = outputString.convertToLowercaseWithoutLocale(); // FIXME: Needs locale to work correctly. [_attrStr replaceCharactersInRange:rangeToReplace withString:outputString]; rangeToReplace.length = outputString.length(); if (rangeToReplace.length) [_attrStr setAttributes:aggregatedAttributesForAncestors(characterData) range:rangeToReplace]; _flags.isSoft = wasSpace; } if (mutstr) CFRelease(mutstr); } void HTMLConverter::_traverseNode(Node& node, unsigned depth, bool embedded) { if (_flags.reachedEnd) return; if (!_flags.reachedStart && !_caches->isAncestorsOfStartToBeConverted(node)) return; unsigned startOffset = 0; unsigned endOffset = UINT_MAX; bool isStart = false; bool isEnd = false; if (&node == m_start.containerNode()) { startOffset = m_start.computeOffsetInContainerNode(); isStart = true; _flags.reachedStart = YES; } if (&node == m_end.containerNode()) { endOffset = m_end.computeOffsetInContainerNode(); isEnd = true; } if (node.isDocumentNode() || node.isDocumentFragment()) { Node* child = node.firstChild(); ASSERT(child == firstChildInComposedTreeIgnoringUserAgentShadow(node)); for (NSUInteger i = 0; child; i++) { if (isStart && i == startOffset) _domRangeStartIndex = [_attrStr length]; if ((!isStart || startOffset <= i) && (!isEnd || endOffset > i)) _traverseNode(*child, depth + 1, embedded); if (isEnd && i + 1 >= endOffset) _flags.reachedEnd = YES; if (_flags.reachedEnd) break; ASSERT(child->nextSibling() == nextSiblingInComposedTreeIgnoringUserAgentShadow(*child)); child = child->nextSibling(); } } else if (is<Element>(node)) { Element& element = downcast<Element>(node); if (_enterElement(element, embedded)) { NSUInteger startIndex = [_attrStr length]; if (_processElement(element, depth)) { if (auto* shadowRoot = shadowRootIgnoringUserAgentShadow(element)) // Traverse through shadow root to detect start and end. _traverseNode(*shadowRoot, depth + 1, embedded); else { auto* child = firstChildInComposedTreeIgnoringUserAgentShadow(node); for (NSUInteger i = 0; child; i++) { if (isStart && i == startOffset) _domRangeStartIndex = [_attrStr length]; if ((!isStart || startOffset <= i) && (!isEnd || endOffset > i)) _traverseNode(*child, depth + 1, embedded); if (isEnd && i + 1 >= endOffset) _flags.reachedEnd = YES; if (_flags.reachedEnd) break; child = nextSiblingInComposedTreeIgnoringUserAgentShadow(*child); } } _exitElement(element, depth, startIndex); } } } else if (node.nodeType() == Node::TEXT_NODE) _processText(downcast<CharacterData>(node)); if (isEnd) _flags.reachedEnd = YES; } void HTMLConverter::_traverseFooterNode(Element& element, unsigned depth) { if (_flags.reachedEnd) return; if (!_flags.reachedStart && !_caches->isAncestorsOfStartToBeConverted(element)) return; unsigned startOffset = 0; unsigned endOffset = UINT_MAX; bool isStart = false; bool isEnd = false; if (&element == m_start.containerNode()) { startOffset = m_start.computeOffsetInContainerNode(); isStart = true; _flags.reachedStart = YES; } if (&element == m_end.containerNode()) { endOffset = m_end.computeOffsetInContainerNode(); isEnd = true; } if (_enterElement(element, YES)) { NSUInteger startIndex = [_attrStr length]; if (_processElement(element, depth)) { auto* child = firstChildInComposedTreeIgnoringUserAgentShadow(element); for (NSUInteger i = 0; child; i++) { if (isStart && i == startOffset) _domRangeStartIndex = [_attrStr length]; if ((!isStart || startOffset <= i) && (!isEnd || endOffset > i)) _traverseNode(*child, depth + 1, true /* embedded */); if (isEnd && i + 1 >= endOffset) _flags.reachedEnd = YES; if (_flags.reachedEnd) break; child = nextSiblingInComposedTreeIgnoringUserAgentShadow(*child); } _exitElement(element, depth, startIndex); } } if (isEnd) _flags.reachedEnd = YES; } void HTMLConverter::_adjustTrailingNewline() { NSUInteger textLength = [_attrStr length]; unichar lastChar = (textLength > 0) ? [[_attrStr string] characterAtIndex:textLength - 1] : 0; BOOL alreadyHasTrailingNewline = (lastChar == '\n' || lastChar == '\r' || lastChar == NSParagraphSeparatorCharacter || lastChar == NSLineSeparatorCharacter || lastChar == WebNextLineCharacter); if (_flags.hasTrailingNewline && !alreadyHasTrailingNewline) [_attrStr replaceCharactersInRange:NSMakeRange(textLength, 0) withString:@"\n"]; } Node* HTMLConverterCaches::cacheAncestorsOfStartToBeConverted(const Position& start, const Position& end) { auto commonAncestor = commonShadowIncludingAncestor(start, end); Node* ancestor = start.containerNode(); while (ancestor) { m_ancestorsUnderCommonAncestor.add(ancestor); if (ancestor == commonAncestor) break; ancestor = ancestor->parentInComposedTree(); } return commonAncestor.get(); } #if !PLATFORM(IOS_FAMILY) static NSFileWrapper *fileWrapperForURL(DocumentLoader* dataSource, NSURL *URL) { if ([URL isFileURL]) return [[[NSFileWrapper alloc] initWithURL:[URL URLByResolvingSymlinksInPath] options:0 error:nullptr] autorelease]; if (dataSource) { if (RefPtr<ArchiveResource> resource = dataSource->subresource(URL)) { NSFileWrapper *wrapper = [[[NSFileWrapper alloc] initRegularFileWithContents:resource->data().createNSData().get()] autorelease]; NSString *filename = resource->response().suggestedFilename(); if (!filename || ![filename length]) filename = suggestedFilenameWithMIMEType(resource->url(), resource->mimeType()); [wrapper setPreferredFilename:filename]; return wrapper; } } NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:URL]; NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; [request release]; if (cachedResponse) { NSFileWrapper *wrapper = [[[NSFileWrapper alloc] initRegularFileWithContents:[cachedResponse data]] autorelease]; [wrapper setPreferredFilename:[[cachedResponse response] suggestedFilename]]; return wrapper; } return nil; } static RetainPtr<NSFileWrapper> fileWrapperForElement(HTMLImageElement& element) { if (CachedImage* cachedImage = element.cachedImage()) { if (SharedBuffer* sharedBuffer = cachedImage->resourceBuffer()) return adoptNS([[NSFileWrapper alloc] initRegularFileWithContents:sharedBuffer->createNSData().get()]); } auto* renderer = element.renderer(); if (is<RenderImage>(renderer)) { auto* image = downcast<RenderImage>(*renderer).cachedImage(); if (image && !image->errorOccurred()) { RetainPtr<NSFileWrapper> wrapper = adoptNS([[NSFileWrapper alloc] initRegularFileWithContents:(__bridge NSData *)image->imageForRenderer(renderer)->tiffRepresentation()]); [wrapper setPreferredFilename:@"image.tiff"]; return wrapper; } } return nil; } #endif namespace WebCore { // This function supports more HTML features than the editing variant below, such as tables. NSAttributedString *attributedStringFromRange(Range& range, NSDictionary** documentAttributes) { return HTMLConverter { range.startPosition(), range.endPosition() }.convert(documentAttributes); } NSAttributedString *attributedStringFromSelection(const VisibleSelection& selection, NSDictionary** documentAttributes) { return attributedStringBetweenStartAndEnd(selection.start(), selection.end(), documentAttributes); } NSAttributedString *attributedStringBetweenStartAndEnd(const Position& start, const Position& end, NSDictionary** documentAttributes) { return HTMLConverter { start, end }.convert(documentAttributes); } #if !PLATFORM(IOS_FAMILY) // This function uses TextIterator, which makes offsets in its result compatible with HTML editing. NSAttributedString *editingAttributedStringFromRange(Range& range, IncludeImagesInAttributedString includeOrSkipImages) { NSFontManager *fontManager = [NSFontManager sharedFontManager]; NSMutableAttributedString *string = [[NSMutableAttributedString alloc] init]; NSUInteger stringLength = 0; RetainPtr<NSMutableDictionary> attrs = adoptNS([[NSMutableDictionary alloc] init]); for (TextIterator it(&range); !it.atEnd(); it.advance()) { RefPtr<Range> currentTextRange = it.range(); Node& startContainer = currentTextRange->startContainer(); Node& endContainer = currentTextRange->endContainer(); int startOffset = currentTextRange->startOffset(); int endOffset = currentTextRange->endOffset(); if (includeOrSkipImages == IncludeImagesInAttributedString::Yes) { if (&startContainer == &endContainer && (startOffset == endOffset - 1)) { Node* node = startContainer.traverseToChildAt(startOffset); if (is<HTMLImageElement>(node)) { RetainPtr<NSFileWrapper> fileWrapper = fileWrapperForElement(downcast<HTMLImageElement>(*node)); NSTextAttachment *attachment = [[NSTextAttachment alloc] initWithFileWrapper:fileWrapper.get()]; [string appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]]; [attachment release]; } } } int currentTextLength = it.text().length(); if (!currentTextLength) continue; RenderObject* renderer = startContainer.renderer(); ASSERT(renderer); if (!renderer) continue; const RenderStyle& style = renderer->style(); if (style.textDecorationsInEffect() & TextDecoration::Underline) [attrs.get() setObject:[NSNumber numberWithInteger:NSUnderlineStyleSingle] forKey:NSUnderlineStyleAttributeName]; if (style.textDecorationsInEffect() & TextDecoration::LineThrough) [attrs.get() setObject:[NSNumber numberWithInteger:NSUnderlineStyleSingle] forKey:NSStrikethroughStyleAttributeName]; if (auto font = style.fontCascade().primaryFont().getCTFont()) [attrs.get() setObject:toNSFont(font) forKey:NSFontAttributeName]; else [attrs.get() setObject:[fontManager convertFont:WebDefaultFont() toSize:style.fontCascade().primaryFont().platformData().size()] forKey:NSFontAttributeName]; Color foregroundColor = style.visitedDependentColorWithColorFilter(CSSPropertyColor); if (foregroundColor.isVisible()) [attrs.get() setObject:nsColor(foregroundColor) forKey:NSForegroundColorAttributeName]; else [attrs.get() removeObjectForKey:NSForegroundColorAttributeName]; Color backgroundColor = style.visitedDependentColorWithColorFilter(CSSPropertyBackgroundColor); if (backgroundColor.isVisible()) [attrs.get() setObject:nsColor(backgroundColor) forKey:NSBackgroundColorAttributeName]; else [attrs.get() removeObjectForKey:NSBackgroundColorAttributeName]; RetainPtr<NSString> text; if (style.nbspMode() == NBSPMode::Normal) text = it.text().createNSStringWithoutCopying(); else text = it.text().toString().replace(noBreakSpace, ' '); [string replaceCharactersInRange:NSMakeRange(stringLength, 0) withString:text.get()]; [string setAttributes:attrs.get() range:NSMakeRange(stringLength, currentTextLength)]; stringLength += currentTextLength; } return [string autorelease]; } #endif }