html - 将属性字符串转换为 "simple"标记的 html

标签 html swift markdown nsattributedstring

我想像这样将 NSAttributedString 转换为 html:

This is a <i>string</i> with some <b>simple</b> <i><b>html</b></i> tags in it.

不幸的是,如果您使用苹果的内置系统,它会生成冗长的基于 css 的 html。 (以下示例供引用。)

那么如何从 NSAttributedString 生成简单的标记 html 呢?

我为此编写了一个非常冗长、脆弱的调用,这是一个糟糕的解决方案。

func simpleTagStyle(fromNSAttributedString att: NSAttributedString)->String {

    // verbose, fragile solution

    // essentially, iterate all the attribute ranges in the attString
    // make a note of what style they are, bold italic etc
    // (totally ignore any not of interest to us)
    // then basically get the plain string, and munge it for those ranges.
    // be careful with the annoying "multiple attribute" case
    // (an alternative would be to repeatedly munge out attributed ranges
    // one by one until there are none left.)

    let rangeAll = NSRange(location: 0, length: att.length)

    // make a note of all of the ranges of bold/italic
    // (use a tuple to remember which is which)
    var allBlocks: [(NSRange, String)] = []

    att.enumerateAttribute(
        NSFontAttributeName,
        in: rangeAll,
        options: .longestEffectiveRangeNotRequired
        )
            { value, range, stop in

            handler: if let font = value as? UIFont {

                let b = font.fontDescriptor.symbolicTraits.contains(.traitBold)
                let i = font.fontDescriptor.symbolicTraits.contains(.traitItalic)

                if b && i {
                    allBlocks.append( (range, "bolditalic") )
                    break handler   // take care not to duplicate
                }

                if b {
                    allBlocks.append( (range, "bold") )
                    break handler
                }

                if i {
                    allBlocks.append( (range, "italic") )
                    break handler
                }
            }

        }

    // traverse those backwards and munge away

    var plainString = att.string

    for oneBlock in allBlocks.reversed() {

        let r = oneBlock.0.range(for: plainString)!

        let w = plainString.substring(with: r)

        if oneBlock.1 == "bolditalic" {
            plainString.replaceSubrange(r, with: "<b><i>" + w + "</i></b>")
        }

        if oneBlock.1 == "bold" {
            plainString.replaceSubrange(r, with: "<b>" + w + "</b>")
        }

        if oneBlock.1 == "italic" {
            plainString.replaceSubrange(r, with: "<i>" + w + "</i>")
        }

    }

    return plainString
}

下面是如何使用 Apple 的内置系统,不幸的是,它会生成完整的 CSS 等。

x = ... your NSAttributedText
var resultHtmlText = ""
do {

    let r = NSRange(location: 0, length: x.length)
    let att = [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType]

    let d = try x.data(from: r, documentAttributes: att)

    if let h = String(data: d, encoding: .utf8) {
        resultHtmlText = h
    }
}
catch {
    print("utterly failed to convert to html!!! \n>\(x)<\n")
}
print(resultHtmlText)

示例输出....

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<title></title>
<meta name="Generator" content="Cocoa HTML Writer">
<style type="text/css">
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px 'Some Font'}
span.s1 {font-family: 'SomeFont-ItalicOrWhatever'; font-weight: normal; font-style: normal; font-size: 14.00pt}
span.s2 {font-family: 'SomeFont-SemiboldItalic'; font-weight: bold; font-style: italic; font-size: 14.00pt}
</style>
</head>
<body>
<p class="p1"><span class="s1">So, </span><span class="s2">here is</span><span class="s1"> some</span> stuff</p>
</body>
</html>

最佳答案

根据 enumerateAttribute:inRange:options:usingBlock: 的文档,尤其是讨论部分,其中指出:

If this method is sent to an instance of NSMutableAttributedString, mutation (deletion, addition, or change) is allowed, as long as it is within the range provided to the block; after a mutation, the enumeration continues with the range immediately following the processed range, after the length of the processed range is adjusted for the mutation. (The enumerator basically assumes any change in length occurs in the specified range.) For example, if block is called with a range starting at location N, and the block deletes all the characters in the supplied range, the next call will also pass N as the index of the range.

换句话说,在闭包/ block 中,使用 range ,您可以删除/替换那里的字符。操作系统将在范围的那一端放置一个标记。完成修改后,它将计算标记的新范围,以便枚举的下一次迭代将从该新标记开始。 因此,您不必将所有范围都保留在一个数组中,然后通过向后替换而不修改范围来应用更改。不要打扰你,这些方法已经做到了。

我不是 Swift 开发人员,我更像是 Objective-C 开发人员。所以我的 Swift 代码可能不遵守所有“Swift 规则”,并且可能有点难看(可选、包装等做得不好,if let 未完成等)

这是我的解决方案:

func attrStrSimpleTag() -> Void {

    let htmlStr = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\"> <html> <head> <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"> <meta http-equiv=\"Content-Style-Type\" content=\"text/css\"> <title></title> <meta name=\"Generator\" content=\"Cocoa HTML Writer\"> <style type=\"text/css\"> p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px 'Some Font'} span.s1 {font-family: 'SomeFont-ItalicOrWhatever'; font-weight: normal; font-style: normal; font-size: 14.00pt} span.s2 {font-family: 'SomeFont-SemiboldItalic'; font-weight: bold; font-style: italic; font-size: 14.00pt} </style> </head> <body> <p class=\"p1\"><span class=\"s1\">So, </span><span class=\"s2\">here is</span><span class=\"s1\"> some</span> stuff</p> </body></html>"
    let attr = try! NSMutableAttributedString.init(data: htmlStr.data(using: .utf8)!,
                                                   options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
                                                   documentAttributes: nil)
    print("Attr: \(attr)")
    attr.enumerateAttribute(NSFontAttributeName, in: NSRange.init(location: 0, length: attr.length), options: []) { (value, range, stop) in
        if let font = value as? UIFont {
            print("font found:\(font)")
            let isBold = font.fontDescriptor.symbolicTraits.contains(.traitBold)
            let isItalic = font.fontDescriptor.symbolicTraits.contains(.traitItalic)
            let occurence = attr.attributedSubstring(from: range).string
            let replacement = self.formattedString(initialString: occurence, bold: isBold, italic: isItalic)
            attr.replaceCharacters(in: range, with: replacement)
        }
    };

    let taggedString = attr.string
    print("taggedString: \(taggedString)")

}

func formattedString(initialString:String, bold: Bool, italic: Bool) -> String {
    var retString = initialString
    if bold {
        retString = "<b>".appending(retString)
        retString.append("</b>")
    }
    if italic
    {
        retString = "<i>".appending(retString)
        retString.append("</i>")
    }

    return retString
}

输出(对于最后一个,其他两个打印仅用于调试):

$> taggedString: So, <i><b>here is</b></i> some stuff

编辑: Objective-C 版本(写得很快,可能有些问题)。

-(void)attrStrSimpleTag
{
    NSString *htmlStr = @"<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\"> <html> <head> <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"> <meta http-equiv=\"Content-Style-Type\" content=\"text/css\"> <title></title> <meta name=\"Generator\" content=\"Cocoa HTML Writer\"> <style type=\"text/css\"> p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px 'Some Font'} span.s1 {font-family: 'SomeFont-ItalicOrWhatever'; font-weight: normal; font-style: normal; font-size: 14.00pt} span.s2 {font-family: 'SomeFont-SemiboldItalic'; font-weight: bold; font-style: italic; font-size: 14.00pt} </style> </head> <body> <p class=\"p1\"><span class=\"s1\">So, </span><span class=\"s2\">here is</span><span class=\"s1\"> some</span> stuff</p> </body></html>";
    NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithData:[htmlStr dataUsingEncoding:NSUTF8StringEncoding]
                                                                              options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType}
                                                                   documentAttributes:nil
                                                                                error:nil];
    NSLog(@"Attr: %@", attr);

    [attr enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, [attr length]) options:0 usingBlock:^(id  _Nullable value, NSRange range, BOOL * _Nonnull stop) {
        UIFont *font = (UIFont *)value;
        NSLog(@"Font found: %@", font);
        BOOL isBold =  UIFontDescriptorTraitBold & [[font fontDescriptor] symbolicTraits];
        BOOL isItalic =  UIFontDescriptorTraitItalic & [[font fontDescriptor] symbolicTraits];
        NSString *occurence = [[attr attributedSubstringFromRange:range] string];
        NSString *replacement = [self formattedStringWithString:occurence isBold:isBold andItalic:isItalic];
        [attr replaceCharactersInRange:range withString:replacement];
    }];

    NSString *taggedString = [attr string];
    NSLog(@"taggedString: %@", taggedString);
}


-(NSString *)formattedStringWithString:(NSString *)string isBold:(BOOL)isBold andItalic:(BOOL)isItalic
{
    NSString *retString = string;
    if (isBold)
    {
        retString = [NSString stringWithFormat:@"<b>%@</b>", retString];
    }
    if (isItalic)
    {
        retString = [NSString stringWithFormat:@"<i>%@</i>", retString];
    }
    return retString;
}

2020 年 1 月编辑:
通过更简单的修改和 Swift 5 更新了代码,添加了对两种新效果(下划线/删除线)的支持。

// MARK: In one loop
extension NSMutableAttributedString {
    func htmlSimpleTagString() -> String {
        enumerateAttributes(in: fullRange(), options: []) { (attributes, range, pointeeStop) in
            let occurence = self.attributedSubstring(from: range).string
            var replacement: String = occurence
            if let font = attributes[.font] as? UIFont {
                replacement = self.font(initialString: replacement, fromFont: font)
            }
            if let underline = attributes[.underlineStyle] as? Int {
                replacement = self.underline(text: replacement, fromStyle: underline)
            }
            if let striked = attributes[.strikethroughStyle] as? Int {
                replacement = self.strikethrough(text: replacement, fromStyle: striked)
            }
            self.replaceCharacters(in: range, with: replacement)
        }
        return self.string
    }
}

// MARK: In multiple loop
extension NSMutableAttributedString {
    func htmlSimpleTagString(options: [NSAttributedString.Key]) -> String {
        if options.contains(.underlineStyle) {
            enumerateAttribute(.underlineStyle, in: fullRange(), options: []) { (value, range, pointeeStop) in
                let occurence = self.attributedSubstring(from: range).string
                guard let style = value as? Int else { return }
                if NSUnderlineStyle(rawValue: style) == NSUnderlineStyle.styleSingle {
                    let replacement = self.underline(text: occurence, fromStyle: style)
                    self.replaceCharacters(in: range, with: replacement)
                }
            }
        }
        if options.contains(.strikethroughStyle) {
            enumerateAttribute(.strikethroughStyle, in: fullRange(), options: []) { (value, range, pointeeStop) in
                let occurence = self.attributedSubstring(from: range).string
                guard let style = value as? Int else { return }
                let replacement = self.strikethrough(text: occurence, fromStyle: style)
                self.replaceCharacters(in: range, with: replacement)
            }
        }
        if options.contains(.font) {
            enumerateAttribute(.font, in: fullRange(), options: []) { (value, range, pointeeStop) in
                let occurence = self.attributedSubstring(from: range).string
                guard let font = value as? UIFont else { return }
                let replacement = self.font(initialString: occurence, fromFont: font)
                self.replaceCharacters(in: range, with: replacement)
            }
        }
        return self.string

    }
}

//MARK: Replacing
extension NSMutableAttributedString {

    func font(initialString: String, fromFont font: UIFont) -> String {
        let isBold = font.fontDescriptor.symbolicTraits.contains(.traitBold)
        let isItalic = font.fontDescriptor.symbolicTraits.contains(.traitItalic)
        var retString = initialString
        if isBold {
            retString = "<b>" + retString + "</b>"
        }
        if isItalic {
            retString = "<i>" + retString + "</i>"
        }
        return retString
    }

    func underline(text: String, fromStyle style: Int) -> String {
        return "<u>" + text + "</u>"
    }

    func strikethrough(text: String, fromStyle style: Int) -> String {
        return "<s>" + text + "</s>"
    }
}

//MARK: Utility
extension NSAttributedString {
    func fullRange() -> NSRange {
        return NSRange(location: 0, length: self.length)
    }
}

使用混合标签测试的简单 HTML:"This is <i>ITALIC</i> with some <b>BOLD</b> <b><i>BOLDandITALIC</b></i> <b>BOLD<u>UNDERLINEandBOLD</b>RESTUNDERLINE</u> in it."

解决方案带来了两种方法:一种做一个循环,另一种做多个循环,但是对于混合标签,结果可能很奇怪。检查之前提供的示例不同的渲染。

关于html - 将属性字符串转换为 "simple"标记的 html,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44027651/

相关文章:

file - 如何使用 markdown 语法链接到任何本地文件?

html - 如何在不渲染的情况下在 Markdown 文件中键入 html?

css - 固定的 div 超出包装

objective-c - 如何在 Objective-C 中使用 Swift 字符串枚举?

ios - iPhone/iPad的UILabel如何设置粗体和斜体?

SwiftUI:按钮圆角边缘的边框尺寸加倍

css - 用自定义字形替换 <hr> 行

javascript - 如何使用显示 ="none"显示隐藏的元素,这些元素存储在数组中而不丢失其旧属性

javascript - 单击菜单项时将类添加到 div

javascript - 如何获取无效 HTML 日期输入的值?