iOS MarkDown解析

1.效果展示

MarkDown组件库地址

排版1
排版2
排版3
排版4

2.调用

2.1 NSString+WPMarkDownParse入口

WPMarkDownParse主要是提供了一个NSString的分类,方便调用;真正的入口在WPMarkDownParseFactory

2.2 WPMarkDownParseFactory

2.2.1 同步异步入口

+ (NSMutableAttributedString *)parseMarkDownWithText:(NSString *)text;
+ (NSMutableAttributedString *)parseMarkDownWithText:(NSString *)text fontSize:(CGFloat)fontSize width:(CGFloat)width;
- (void)parseMarkDownWithText:(NSString *)text finishBlock:(void (^)(NSMutableAttributedString * string))block;
- (void)parseMarkDownWithText:(NSString *)text fontSize:(CGFloat)fontSize width:(CGFloat)width finishBlock:(void (^)(NSMutableAttributedString * string))block;

2.2.2 parseMarkDownWithText

+ (NSMutableAttributedString *)parseMarkDownWithText:(NSString *)text fontSize:(CGFloat)fontSize width:(CGFloat)width{
    NSArray * parseArray = [self setUpParseArray];
    for (WPMarkDownBaseParse * parseModel in parseArray) {
        [parseModel configFontSize:fontSize width:width];
        [parseModel segmentString:&text];
    }
    
    [self replaceBackslash:&text];
    NSMutableAttributedString * attributedString = [[NSMutableAttributedString alloc] initWithString:text];
    [self setAttributedDefaultFont:attributedString fontSize:fontSize];
    
    for (WPMarkDownBaseParse * parseModel in parseArray) {
        [parseModel setAttributedString:attributedString];
    }
    return attributedString;
}

2.2.3 setUpParseArray初始化

WPMarkDownParseImage:解析图片
WPMarkDownParseLink:解析链接
WPMarkDownParseQuoteParagraph:解析段落引用
WPMarkDownParseCodeBlock:解析代码块
WPMarkDownParseBold:加粗
WPMarkDownParseItalic:斜体
WPMarkDownParseTitle:标题
WPMarkDownParseDisorder:无序
WPMarkDownParseOrder:有序

所有的解析类继承自WPMarkDownBaseParse,使用策略模式、模板模式与工厂模式结合进行解析。

2.2.4 策略模式方法

//解析策略模式
@protocol WPMarkDownParseStrageInterface <NSObject>
- (NSString *)replace:(NSString *)text;
- (void)segmentString:(NSArray *)separatedArray text:(NSString *)text;//分割字符串
- (void)setAttributedString:(NSMutableAttributedString *)attributedString;
@end
  1. 每个类按symbol进行分割,如果separatedArray不为空,则进行解析,判断是否满足条件,加入self.segmentArray中。
  2. 替换掉markdown的标识符,如链接[百度](https:baidu.com),只能显示百度,字体高亮,点击能跳转到WebView.

2.2.5 replaceBackslash

替换掉转义字符\,即出现反斜杠,都不解析。

2.2.6 setAttributedString

attributedString 是所有都替换完,才生产的attributedString。
策略模式使得每个类setAttributedString能够设置对应的属性,如图片,高亮、斜体等。

3. 链接、图片解析过程

3.1 WPMarkDownParseLink

  1. 在setUpParseArray初始化,WPMarkDownParseLink添加到解析parseArray数组中
  2. 配置fontSize与width
for (WPMarkDownBaseParse * parseModel in parseArray) {
        [parseModel configFontSize:fontSize width:width];
        [parseModel segmentString:&text];
    }
  1. segmentString 按symbo分割,如果分割separatedArray不为空,则wp_markdownParseSegmentString进行解析,解析出url于对应的title的WPMarkDownParseLinkModel,添加到segmentArray
- (void)wp_markdownParseSegmentString:(NSArray *)separatedArray text:(NSString *)text{
    for (int i = 0; i<separatedArray.count-1; i++) {
        NSString * leftString = separatedArray[I];
        if ([self isBackslash:leftString]) {
            continue;
        }
        WPMarkDownParseLinkModel * urlModel = [[WPMarkDownParseLinkModel alloc] initWithSymbol:self.symbol];
        NSArray * leftStringSeparateArray = [leftString componentsSeparatedByString:@"["];
        if (leftStringSeparateArray.count>0) {
            urlModel.text = leftStringSeparateArray.lastObject;
        }
        NSArray * rightStringSepartedArray = [separatedArray[i+1] componentsSeparatedByString:@")"];
        if (rightStringSepartedArray.count>0) {
            urlModel.url = rightStringSepartedArray.firstObject;
        }
        if (urlModel.text.length && urlModel.url.length) {
            [self.segmentArray addObject:urlModel];//当文字与url都不为空时,才算解析成功
        }        
    }
}
  • 遍历separatedArray
  • isBackslash上一个字符是转义字符,不添加urlModel
  • 左则的字符查找符号[,右侧查找)
  • 当两者都找到时,则匹配成功
  1. 替换用[]的内容,替换内容。即wp_markdownParseReplace
  • 遍历segmentArray,逐个替换。
  • willBeReplacedString、replaceString用了模板模式,因为每个解析略有不同。
  1. replaceBackslash替换掉转义字符\,生成NSMutableAttributedString
  2. 设置一个默认字体大小setAttributedDefaultFont
  3. wp_markdownParseSetAttributedString为链接添加下划线,和点击调整事件
- (void)wp_markdownParseSetAttributedString:(NSMutableAttributedString *)attributedString{
    NSString * text = attributedString.string;
    [self.segmentArray enumerateObjectsUsingBlock:^(WPMarkDownParseLinkModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSRange range = [text rangeOfString:obj.text];
        [attributedString wp_makeAttributed:^(WPMutableAttributedStringMaker * _Nullable make) {
            make.textColor([UIColor blueColor],range);
            make.underlineStyle(NSUnderlineStyleSingle,[UIColor blueColor],range);
        }];
        [attributedString yy_setTextHighlightRange:range color:[UIColor blueColor] backgroundColor:[UIColor clearColor] tapAction:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
            [WPWKWebViewController pushWKWebViewController:obj.url title:obj.text];
        }];
    }];
}

3.2 WPMarkDownParseImage

图片解析或其他解析与链接解析大致相同,区别在于每个细节内容都不相同。

  1. WPMarkDownParseImage添加到parseArray
  2. 配置字体和宽度configFontSize:fontSize:width
  3. segmentString木模模式,分割生成segmentArray,并匹配字符
- (void)wp_markdownParseSegmentString:(NSArray *)separatedArray text:(NSString *)text{
    
    for (int i = 0; i<separatedArray.count-1; i++) {
        
        NSString * leftString = separatedArray[I];
        if ([self isBackslash:leftString]) {
            continue;
        }
        WPMarkDownParseImageModel * urlModel = [[WPMarkDownParseImageModel alloc] initWithSymbol:self.symbol];
        NSArray * leftStringSeparteds = [leftString componentsSeparatedByString:@"!["];
        if (leftStringSeparteds.count>1) {
            urlModel.text = leftStringSeparteds.lastObject;
        }
        NSArray * rightSepartedArray = [separatedArray[i+1] componentsSeparatedByString:@")"];
        if (rightSepartedArray.count>0) {
            urlModel.url = rightSepartedArray.firstObject;
        }
        if (urlModel.text.length && urlModel.url.length) {
            [self.segmentArray addObject:urlModel];//当文字与url都不为空时,才算解析成功
        }
    }
}
  • 遍历separatedArray
  • isBackslash上一个字符是转义字符,不添加urlModel
  • 左则的字符查找符号![,右侧查找)
  • 当两者都找到时,则匹配成功

与链接不同的是,图片的左边是![,所以防止链接被图片的解析覆盖,所以需要把图片解析放在链接解析前面。

  1. 设置UIImageView
- (void)wp_markdownParseSetAttributedString:(NSMutableAttributedString *)attributedString{
    NSString * text = attributedString.string;
    [self.segmentArray enumerateObjectsUsingBlock:^(WPMarkDownParseLinkModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSRange range = [text rangeOfString:obj.text];
        
        {   /*
             设置图片,现在是固定宽高,可让url后带上宽高
             */
            UIImageView * imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.defaultWidth, self.defaultWidth*0.68)];
            [imageView sd_setImageWithURL:[NSURL URLWithString:obj.url] completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
                
            }];
            
            imageView.backgroundColor = [UIColor whiteColor];
            NSMutableAttributedString *attachText = [NSMutableAttributedString yy_attachmentStringWithContent:imageView contentMode:UIViewContentModeCenter attachmentSize:imageView.frame.size alignToFont:[UIFont systemFontOfSize:self.defaultFontSize] alignment:YYTextVerticalAlignmentCenter];
            [attachText appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:nil]];
            [attributedString insertAttributedString:attachText atIndex:range.location];
        }
        
        {//处理描述文字
            range = [text rangeOfString:obj.text];
            [attributedString wp_makeAttributed:^(WPMutableAttributedStringMaker * _Nullable make) {
                make.textFont(self.defaultFontSize-2,range);
                make.textColor([UIColor grayColor],range);
                
                CGFloat width = [self calculateWidth:obj.text fontSize:self.defaultFontSize];
                WPMutableParagraphStyleModel * styleModel = [self paragraphStyleModel:width];
                make.paragraphStyle([styleModel createParagraphStyle],range);
            }];
        }
    }];
}

- (WPMutableParagraphStyleModel *)paragraphStyleModel:(CGFloat)width{
    
    WPMutableParagraphStyleModel * styleModel = [WPMutableParagraphStyleModel new];
    styleModel.headIndent =  (self.defaultWidth-width)/2;//整体缩进(首行除外)
    styleModel.firstLineHeadIndent = (self.defaultWidth-width)/2;
    styleModel.alignment = NSTextAlignmentJustified;
    return styleModel;
}

- (CGFloat)calculateWidth:(NSString *)text fontSize:(CGFloat)fontSize{
    CGRect rect = [text boundingRectWithSize:CGSizeMake(0, 16) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:fontSize]} context:nil];
    return rect.size.width;
}
  • 这里借助了YYText,直接在对应的位置插入了UIImageView,使用SDWebImage进行下载。
  • 这里的图片宽度是根据外面传入的,高度比例固定,实际情况可根据服务端在url地址上直接返回比例,解决图片压缩问题。
  • 文字设置成了灰色,字号缩小2号,位置居中

3.3 其他解析

其他解析与图片和链接解析类似,细节略有不同。采用统一模板和策略。

3.4 组装

+ (NSMutableAttributedString *)parseMarkDownWithText:(NSString *)text fontSize:(CGFloat)fontSize width:(CGFloat)width{
    NSArray * parseArray = [self setUpParseArray];
    for (WPMarkDownBaseParse * parseModel in parseArray) {
        [parseModel configFontSize:fontSize width:width];
        [parseModel segmentString:&text];
    }
    
    [self replaceBackslash:&text];
    NSMutableAttributedString * attributedString = [[NSMutableAttributedString alloc] initWithString:text];
    [self setAttributedDefaultFont:attributedString fontSize:fontSize];
    
    for (WPMarkDownBaseParse * parseModel in parseArray) {
        [parseModel wp_markdownParseSetAttributedString:attributedString];
    }
    return attributedString;
}
  • 所有的字符解析完成,替换反斜杠,最后生成attributedString
  • setAttributedDefaultFont设置默认字号
  • wp_markdownParseSetAttributedString统一设置对应的属性

4.链接与标题单元测试

@interface WPMarkDownParseStringTest : XCTestCase
{
    WPMarkDownParseLink * parseLink;
    WPMarkDownParseTitle * parseTitle;
}
@end

@implementation WPMarkDownParseStringTest

- (void)setUp {
    // Put setup code here. This method is called before the invocation of each test method in the class.
    parseLink = [[WPMarkDownParseLink alloc] initWithSymbol:@"]("];
    parseTitle = [[WPMarkDownParseTitle alloc] initWithSymbol:@"#"];
}

- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    parseLink = nil;
}

- (void)testExample {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}

- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}

#pragma mark - 解析URL

- (void)testSpiltOneUrl{
    NSString * text = @"计划:[事件传递和事件响应](https://blog.csdn.net/suma110/article/details/99290799)";
    
    [parseLink segmentString:&text];
    
    WPMarkDownParseLinkModel * urlModel = parseLink.segmentArray.firstObject;
    XCTAssertTrue([urlModel.text isEqualToString:@"事件传递和事件响应"],@"text分割正确");
    XCTAssertTrue([urlModel.url isEqualToString:@"https://blog.csdn.net/suma110/article/details/99290799"],@"url分割正确");
}

- (void)testSpiltTwoUrl{
    NSString * text = @"计划:[事件传递和事件响应](https://blog.csdn.net/suma110/article/details/99290799)中间级还有很多[事件传递和事件响应2](https://blog.csdn.net/suma110/article/details/99290798)";
    
    [parseLink segmentString:&text];
        
    WPMarkDownParseLinkModel * urlModel = parseLink.segmentArray.firstObject;
    XCTAssertTrue([urlModel.text isEqualToString:@"事件传递和事件响应"],@"text分割正确");
    XCTAssertTrue([urlModel.url isEqualToString:@"https://blog.csdn.net/suma110/article/details/99290799"],@"url分割正确");
    
    WPMarkDownParseLinkModel * twoUrlModel = parseLink.segmentArray[1];
    XCTAssertTrue([twoUrlModel.text isEqualToString:@"事件传递和事件响应2"],@"text分割正确");
    XCTAssertTrue([twoUrlModel.url isEqualToString:@"https://blog.csdn.net/suma110/article/details/99290798"],@"url分割正确");
}

- (void)testSpiltOneUrl2{
    NSString * text = @"[事件传递和事件响应](https://blog.csdn.net/suma110/article/details/99290799)";
    
    [parseLink segmentString:&text];
    
    WPMarkDownParseLinkModel * urlModel = parseLink.segmentArray.firstObject;
    XCTAssertTrue([urlModel.text isEqualToString:@"事件传递和事件响应"],@"text分割正确");
    XCTAssertTrue([urlModel.url isEqualToString:@"https://blog.csdn.net/suma110/article/details/99290799"],@"url分割正确");
}

- (void)testSpiltTwoUrl2{
    NSString * text = @"1.Textview展示超链接,除了链接外,其他区域父视图响应\n替补方案:没有超链接的,关闭响应。\n2.scrollView添加tableView,scrollView支持横向,tableView竖向滚动,在数据少时,不能下拉刷新。\n[嵌套UIScrollview的滑动冲突解决方案](https://www.jianshu.com/p/040772693872)\n[iOS 嵌套UIScrollview的滑动冲突另一种解决方案](https://www.jianshu.com/p/df01610b4e73)";
    
    [parseLink segmentString:&text];
    
    WPMarkDownParseLinkModel * urlModel = parseLink.segmentArray.firstObject;
    XCTAssertTrue([urlModel.text isEqualToString:@"嵌套UIScrollview的滑动冲突解决方案"],@"text分割正确");
    XCTAssertTrue([urlModel.url isEqualToString:@"https://www.jianshu.com/p/040772693872"],@"url分割正确");
    
    WPMarkDownParseLinkModel * twoUrlModel = parseLink.segmentArray[1];
    XCTAssertTrue([twoUrlModel.text isEqualToString:@"iOS 嵌套UIScrollview的滑动冲突另一种解决方案"],@"text分割正确");
    XCTAssertTrue([twoUrlModel.url isEqualToString:@"https://www.jianshu.com/p/df01610b4e73"],@"url分割正确");
}

#pragma mark - 解析title

- (void)testParseOneTitle{
    NSString * text = @"#1.Textview展示超链接\n2.scrollView添加tableView,scrollView支持横向,tableView竖向滚动,在数据少时,不能下拉刷新。\n[嵌套UIScrollview的滑动冲突解决方案](https://www.jianshu.com/p/040772693872)\n[iOS 嵌套UIScrollview的滑动冲突另一种解决方案](https://www.jianshu.com/p/df01610b4e73)";
    
    [parseTitle segmentString:&text];
    
    WPMarkDownParseLinkModel * titleModel =  parseTitle.segmentArray.firstObject;
    XCTAssertTrue([titleModel.text isEqualToString:@"1.Textview展示超链接"]);
}

- (void)testParseTwoTitle{
    NSString * text = @"#1.Textview展示超链接\n#2.scrollView添加tableView,scrollView支持横向,tableView竖向滚动,在数据少时,不能下拉刷新。\n[嵌套UIScrollview的滑动冲突解决方案](https://www.jianshu.com/p/040772693872)\n[iOS 嵌套UIScrollview的滑动冲突另一种解决方案](https://www.jianshu.com/p/df01610b4e73)";
    
    [parseTitle segmentString:&text];
    
    WPMarkDownParseLinkModel * titleModel =  parseTitle.segmentArray.firstObject;
    WPMarkDownParseLinkModel * titleModel2 =  parseTitle.segmentArray[1];
    XCTAssertTrue([titleModel.text isEqualToString:@"1.Textview展示超链接"]);
    XCTAssertTrue([titleModel2.text isEqualToString:@"2.scrollView添加tableView,scrollView支持横向,tableView竖向滚动,在数据少时,不能下拉刷新。"]);
}

#pragma mark - 解析截取字符

- (void)testSubStringLast3Number{
    WPMarkDownParseOrder * order = [WPMarkDownParseOrder new];
    NSString * text = @"abcd123";
    NSString * subString = [order subStringLastNum:text];
    XCTAssertTrue([subString isEqualToString:@"123"]);
}

- (void)testSubStringLast1Number{
    WPMarkDownParseOrder * order = [WPMarkDownParseOrder new];
    NSString * text = @"abcd1";
    NSString * subString = [order subStringLastNum:text];
    XCTAssertTrue([subString isEqualToString:@"1"]);
}

- (void)testSubString1Number{
    WPMarkDownParseOrder * order = [WPMarkDownParseOrder new];
    NSString * text = @"1";
    NSString * subString = [order subStringLastNum:text];
    XCTAssertTrue([subString isEqualToString:@"1"]);
}

- (void)testSubStringNoNumber{
    WPMarkDownParseOrder * order = [WPMarkDownParseOrder new];
    NSString * text = @"abc";
    NSString * subString = [order subStringLastNum:text];
    XCTAssertTrue([subString isEqualToString:@""]);
}

@end

5. 总结

MarkDown组件库地址

  1. Markdown的解析功能基本完成,但还有很多细节需处理。
  2. 由于按文字匹配,会出现匹配文字。如标题与链接相同时,标题可能会被加上下划线;有序嵌套时效果不是很好,现在是碰到两个\n停止,而实际远不止这些条件。
  3. 文中的富文本链式使用,已经封装成了WPChained
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容

  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,345评论 0 5
  • 概要 64学时 3.5学分 章节安排 电子商务网站概况 HTML5+CSS3 JavaScript Node 电子...
    阿啊阿吖丁阅读 9,024评论 0 3
  • 你是否听过一首音乐感动到热泪盈眶,却又不知为何要流泪,瞬间引起人生的各种共鸣 音乐是沟通人类文化的艺术之一,前几日...
    一颗小鸡蛋阅读 287评论 0 1
  • 当我写这一篇文章时,我已经是浙江金融职业学院的一名学生了,在即将入学之际,我站在大学的开头,展望一下大学四年的学习...
    心中藏了匹自由的野马阅读 189评论 0 0
  • 洞察底层规律,实现跨越式成长,升级认知,迭代操作系统。 这个时代:信息变多、思考变浅,机会变多、竞...
    赵秀荣_3e2b阅读 313评论 0 2