1.效果展示
排版1
排版2
排版3
排版4
2.調(diào)用
2.1 NSString+WPMarkDownParse入口
WPMarkDownParse主要是提供了一個NSString的分類,方便調(diào)用;真正的入口在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:標(biāo)題
WPMarkDownParseDisorder:無序
WPMarkDownParseOrder:有序
所有的解析類繼承自WPMarkDownBaseParse艘儒,使用策略模式雪猪、模板模式與工廠模式結(jié)合進(jìn)行解析嘹害。
2.2.4 策略模式方法
//解析策略模式
@protocol WPMarkDownParseStrageInterface <NSObject>
- (NSString *)replace:(NSString *)text;
- (void)segmentString:(NSArray *)separatedArray text:(NSString *)text;//分割字符串
- (void)setAttributedString:(NSMutableAttributedString *)attributedString;
@end
- 每個類按symbol進(jìn)行分割,如果separatedArray不為空泉手,則進(jìn)行解析,判斷是否滿足條件偶器,加入self.segmentArray中斩萌。
- 替換掉markdown的標(biāo)識符缝裤,如鏈接[百度](https:baidu.com),只能顯示百度颊郎,字體高亮憋飞,點(diǎn)擊能跳轉(zhuǎn)到WebView.
2.2.5 replaceBackslash
替換掉轉(zhuǎn)義字符\,即出現(xiàn)反斜杠姆吭,都不解析榛做。
2.2.6 setAttributedString
attributedString 是所有都替換完,才生產(chǎn)的attributedString内狸。
策略模式使得每個類setAttributedString能夠設(shè)置對應(yīng)的屬性检眯,如圖片,高亮昆淡、斜體等锰瘸。
3. 鏈接、圖片解析過程
3.1 WPMarkDownParseLink
- 在setUpParseArray初始化昂灵,WPMarkDownParseLink添加到解析parseArray數(shù)組中
- 配置fontSize與width
for (WPMarkDownBaseParse * parseModel in parseArray) {
[parseModel configFontSize:fontSize width:width];
[parseModel segmentString:&text];
}
- segmentString 按symbo分割获茬,如果分割separatedArray不為空,則wp_markdownParseSegmentString進(jìn)行解析倔既,解析出url于對應(yīng)的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];//當(dāng)文字與url都不為空時恕曲,才算解析成功
}
}
}
- 遍歷separatedArray
- isBackslash上一個字符是轉(zhuǎn)義字符,不添加urlModel
- 左則的字符查找符號[渤涌,右側(cè)查找)
- 當(dāng)兩者都找到時佩谣,則匹配成功
- 遍歷segmentArray茸俭,逐個替換。
- willBeReplacedString安皱、replaceString用了模板模式调鬓,因?yàn)槊總€解析略有不同。
- replaceBackslash替換掉轉(zhuǎn)義字符\酌伊,生成NSMutableAttributedString
- 設(shè)置一個默認(rèn)字體大小setAttributedDefaultFont
- wp_markdownParseSetAttributedString為鏈接添加下劃線腾窝,和點(diǎn)擊調(diào)整事件
- (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
圖片解析或其他解析與鏈接解析大致相同,區(qū)別在于每個細(xì)節(jié)內(nèi)容都不相同居砖。
- WPMarkDownParseImage添加到parseArray
- 配置字體和寬度configFontSize:fontSize:width
- 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];//當(dāng)文字與url都不為空時奏候,才算解析成功
}
}
}
- 遍歷separatedArray
- isBackslash上一個字符是轉(zhuǎn)義字符循集,不添加urlModel
- 左則的字符查找符號![,右側(cè)查找)
- 當(dāng)兩者都找到時蔗草,則匹配成功
與鏈接不同的是咒彤,圖片的左邊是![,所以防止鏈接被圖片的解析覆蓋疆柔,所以需要把圖片解析放在鏈接解析前面。
- 設(shè)置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];
{ /*
設(shè)置圖片,現(xiàn)在是固定寬高镶柱,可讓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;//整體縮進(jìn)(首行除外)
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婆硬,直接在對應(yīng)的位置插入了UIImageView,使用SDWebImage進(jìn)行下載。
- 這里的圖片寬度是根據(jù)外面?zhèn)魅氲募槔叨缺壤潭ū蚍福瑢?shí)際情況可根據(jù)服務(wù)端在url地址上直接返回比例,解決圖片壓縮問題查吊。
- 文字設(shè)置成了灰色谐区,字號縮小2號,位置居中
3.3 其他解析
其他解析與圖片和鏈接解析類似逻卖,細(xì)節(jié)略有不同宋列。采用統(tǒng)一模板和策略。
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設(shè)置默認(rèn)字號
- wp_markdownParseSetAttributedString統(tǒng)一設(shè)置對應(yīng)的屬性
4.鏈接與標(biāo)題單元測試
@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 = @"計(jì)劃:[事件傳遞和事件響應(yīng)](https://blog.csdn.net/suma110/article/details/99290799)";
[parseLink segmentString:&text];
WPMarkDownParseLinkModel * urlModel = parseLink.segmentArray.firstObject;
XCTAssertTrue([urlModel.text isEqualToString:@"事件傳遞和事件響應(yīng)"],@"text分割正確");
XCTAssertTrue([urlModel.url isEqualToString:@"https://blog.csdn.net/suma110/article/details/99290799"],@"url分割正確");
}
- (void)testSpiltTwoUrl{
NSString * text = @"計(jì)劃:[事件傳遞和事件響應(yīng)](https://blog.csdn.net/suma110/article/details/99290799)中間級還有很多[事件傳遞和事件響應(yīng)2](https://blog.csdn.net/suma110/article/details/99290798)";
[parseLink segmentString:&text];
WPMarkDownParseLinkModel * urlModel = parseLink.segmentArray.firstObject;
XCTAssertTrue([urlModel.text isEqualToString:@"事件傳遞和事件響應(yīng)"],@"text分割正確");
XCTAssertTrue([urlModel.url isEqualToString:@"https://blog.csdn.net/suma110/article/details/99290799"],@"url分割正確");
WPMarkDownParseLinkModel * twoUrlModel = parseLink.segmentArray[1];
XCTAssertTrue([twoUrlModel.text isEqualToString:@"事件傳遞和事件響應(yīng)2"],@"text分割正確");
XCTAssertTrue([twoUrlModel.url isEqualToString:@"https://blog.csdn.net/suma110/article/details/99290798"],@"url分割正確");
}
- (void)testSpiltOneUrl2{
NSString * text = @"[事件傳遞和事件響應(yīng)](https://blog.csdn.net/suma110/article/details/99290799)";
[parseLink segmentString:&text];
WPMarkDownParseLinkModel * urlModel = parseLink.segmentArray.firstObject;
XCTAssertTrue([urlModel.text isEqualToString:@"事件傳遞和事件響應(yīng)"],@"text分割正確");
XCTAssertTrue([urlModel.url isEqualToString:@"https://blog.csdn.net/suma110/article/details/99290799"],@"url分割正確");
}
- (void)testSpiltTwoUrl2{
NSString * text = @"1.Textview展示超鏈接,除了鏈接外盗迟,其他區(qū)域父視圖響應(yīng)\n替補(bǔ)方案:沒有超鏈接的坤邪,關(guān)閉響應(yīng)。\n2.scrollView添加tableView罚缕,scrollView支持橫向艇纺,tableView豎向滾動,在數(shù)據(jù)少時邮弹,不能下拉刷新黔衡。\n[嵌套UIScrollview的滑動沖突解決方案](http://www.reibang.com/p/040772693872)\n[iOS 嵌套UIScrollview的滑動沖突另一種解決方案](http://www.reibang.com/p/df01610b4e73)";
[parseLink segmentString:&text];
WPMarkDownParseLinkModel * urlModel = parseLink.segmentArray.firstObject;
XCTAssertTrue([urlModel.text isEqualToString:@"嵌套UIScrollview的滑動沖突解決方案"],@"text分割正確");
XCTAssertTrue([urlModel.url isEqualToString:@"http://www.reibang.com/p/040772693872"],@"url分割正確");
WPMarkDownParseLinkModel * twoUrlModel = parseLink.segmentArray[1];
XCTAssertTrue([twoUrlModel.text isEqualToString:@"iOS 嵌套UIScrollview的滑動沖突另一種解決方案"],@"text分割正確");
XCTAssertTrue([twoUrlModel.url isEqualToString:@"http://www.reibang.com/p/df01610b4e73"],@"url分割正確");
}
#pragma mark - 解析title
- (void)testParseOneTitle{
NSString * text = @"#1.Textview展示超鏈接\n2.scrollView添加tableView,scrollView支持橫向腌乡,tableView豎向滾動盟劫,在數(shù)據(jù)少時,不能下拉刷新与纽。\n[嵌套UIScrollview的滑動沖突解決方案](http://www.reibang.com/p/040772693872)\n[iOS 嵌套UIScrollview的滑動沖突另一種解決方案](http://www.reibang.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豎向滾動渣锦,在數(shù)據(jù)少時硝岗,不能下拉刷新。\n[嵌套UIScrollview的滑動沖突解決方案](http://www.reibang.com/p/040772693872)\n[iOS 嵌套UIScrollview的滑動沖突另一種解決方案](http://www.reibang.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豎向滾動冗尤,在數(shù)據(jù)少時听盖,不能下拉刷新胀溺。"]);
}
#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. 總結(jié)
- Markdown的解析功能基本完成,但還有很多細(xì)節(jié)需處理皆看。
- 由于按文字匹配仓坞,會出現(xiàn)匹配文字。如標(biāo)題與鏈接相同時腰吟,標(biāo)題可能會被加上下劃線无埃;有序嵌套時效果不是很好,現(xiàn)在是碰到兩個\n停止毛雇,而實(shí)際遠(yuǎn)不止這些條件嫉称。
- 文中的富文本鏈?zhǔn)绞褂茫呀?jīng)封裝成了WPChained