coretext

作者: sunney0 | 来源:发表于2021-02-01 16:39 被阅读0次

    支持图文混排的排版引擎

    改造模版文件

    下面我们来进一步改造,让排版引擎支持对于图片的排版。在上一小节中,我们在设置模版文件的时候,就专门在模板文件里面留了一个名为type的字段,用于表示内容的类型。之前的type的值都是txt,这次,我们增加一个值为img的值,用于表示图片。

    我们将上一节的content.json文件修改为如下内容,增加了 2 个type值为img的配置项。由于是图片的配置项,所以我们不需要设置颜色,字号这些图片不具有的属性,但是,我们另外增加了 3 个图片的配置属性:

    1. 一个名为width的属性,用于设置图片显示的宽度。
    2. 一个名为height的属性,用于设置图片显示的高度。
    3. 一个名为name的属性,用于设置图片的资源名。
    <pre style="font-family: &quot;Source Code Pro&quot;, Consolas, Menlo, Monaco, &quot;Courier New&quot;, monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">[ {
     "type" : "img",
     "width" : 200,
     "height" : 108,
     "name" : "coretext-image-1.jpg"
     },
     { "color" : "blue",
     "content" : " 更进一步地,实际工作中,我们更希望通过一个排版文件,来设置需要排版的文字的 ",
     "size" : 16,
     "type" : "txt"
     },
     { "color" : "red",
     "content" : " 内容、颜色、字体 ",
     "size" : 22,
     "type" : "txt"
     },
     { "color" : "black",
     "content" : " 大小等信息。\n",
     "size" : 16,
     "type" : "txt"
     },
     {
     "type" : "img",
     "width" : 200,
     "height" : 130,
     "name" : "coretext-image-2.jpg"
     },
     { "color" : "default",
     "content" : " 我在开发猿题库应用时,自己定义了一个基于 UBB 的排版模版,但是实现该排版文件的解析器要花费大量的篇幅,考虑到这并不是本章的重点,所以我们以一个较简单的排版文件来讲解其思想。",
     "type" : "txt"
     }
    ]
    </pre>
    

    按理说,图片本身的内容信息中,是包含宽度和高度信息的,为什么我们要在这里指定图片的宽高呢?这主要是因为,在真实的开发中,应用的模版和图片通常是通过服务器获取的,模版是纯文本的内容,获取速度比图片快很多,而图片不但获取速度慢,而且为了省流量,通常的做法是直到需要显示图片的时候,再加载图片内容。

    如果我们不将图片的宽度和高度信息设置在模板里面,那么 CoreText 在排版的时候就无法知道绘制所需要的高度,我们就无法设置CoreTextData类中的height信息,没有高度信息,就会对 UITableView 一类的控件排版造成影响。所以,除非你的应用图片能够保证在绘制前都能全部在本地,否则就应该另外提前提供图片宽度和高度信息。

    在完成模板文件修改后,我们选取两张测试用的图片,分别将其命名为coretext-image-1.jpgcoretext-image-2.jpg(和模板中的值一致),将其拖动增加到工程中。向 Xcode 工程增加图片资源是基础知识,在此就不详细介绍过程了。

    CTLine 与 CTRun

    接下来我们需要改造的是CTFrameParser类,让解析模板文件的方法支持typeimg的配置。

    在改造前,我们先来了解一下CTFrame内部的组成。通过之前的例子,我们可以看到,我们首先通过NSAttributeString和配置信息创建 CTFrameSetter
    然后,再通过CTFrameSetter来创建CTFrame

    CTFrame内部,是由多个CTLine来组成的,每个CTLine代表一行,每个CTLine又是由多个CTRun来组成,每个CTRun代表一组显示风格一致的文本。我们不用手工管理CTLineCTRun的创建过程。

    下图是一个CTLineCTRun的示意图,可以看到,第三行的CTLine是由 2 个CTRun构成的,第一个CTRun为红色大字号的左边部分,第二个CTRun为右边字体较小的部分。

    146_f12_b55.jpg

    虽然我们不用管理CTRun的创建过程,但是我们可以设置某一个具体的CTRunCTRunDelegate来指定该文本在绘制时的高度、宽度、排列对齐方式等信息。

    对于图片的排版,其实 CoreText 本质上不是直接支持的,但是,我们可以在要显示文本的地方,用一个特殊的空白字符代替,同时设置该字体的CTRunDelegate信息为要显示的图片的宽度和高度信息,这样最后生成的CTFrame实例,就会在绘制时将图片的位置预留出来。

    因为我们的CTDisplayView的绘制代码是在drawRect里面的,所以我们可以方便地把需要绘制的图片,用CGContextDrawImage方法直接绘制出来就可以了。

    改造模版解析类

    在了解了以上原理后,我们就可以开始进行改造了。

    我们需要做的工作包括:

    1. 改造CTFrameParserparseTemplateFile:(NSString *)path config:(CTFrameParserConfig*)config;方法,使其支持对typeimg的节点解析。并且对typeimg的节点,设置其CTRunDelegate信息,使其在绘制时,为图片预留相应的空白位置。
    2. 改造CoreTextData类,增加图片相关的信息,并且增加计算图片绘制区域的逻辑。
    3. 改造CTDisplayView类,增加绘制图片相关的逻辑。

    首先介绍对于CTFrameParser的改造:

    我们修改了parseTemplateFile方法,增加了一个名为imageArray的参数来保存解析时的图片信息。

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">+ (CoreTextData *)parseTemplateFile:(NSString )path config:(CTFrameParserConfig)config {
    NSMutableArray *imageArray = [NSMutableArray array];
    NSAttributedString *content = [self loadTemplateFile:path config:config imageArray:imageArray];
    CoreTextData *data = [self parseAttributedContent:content config:config];
    data.imageArray = imageArray;
    return data;
    }
    </pre>

    |

    接着我们修改loadTemplateFile方法,增加了对于typeimg的节点处理逻辑,该逻辑主要做 2 件事情:

    1. 保存当前图片节点信息到imageArray变量中
    2. 新建一个空白的占位符。

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">+ (NSAttributedString *)loadTemplateFile:(NSString )path
    config:(CTFrameParserConfig
    )config
    imageArray:(NSMutableArray *)imageArray {
    NSData *data = [NSData dataWithContentsOfFile:path];
    NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
    if (data) {
    NSArray *array = [NSJSONSerialization JSONObjectWithData:data
    options:NSJSONReadingAllowFragments
    error:nil];
    if ([array isKindOfClass:[NSArray class]]) {
    for (NSDictionary *dict in array) {
    NSString *type = dict[@"type"];
    if ([type isEqualToString:@"txt"]) {
    NSAttributedString *as =
    [self parseAttributedContentFromNSDictionary:dict
    config:config];
    [result appendAttributedString:as];
    } else if ([type isEqualToString:@"img"]) {
    // 创建 CoreTextImageData
    CoreTextImageData *imageData = [[CoreTextImageData alloc] init];
    imageData.name = dict[@"name"];
    imageData.position = [result length];
    [imageArray addObject:imageData];
    // 创建空白占位符,并且设置它的 CTRunDelegate 信息
    NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config];
    [result appendAttributedString:as];
    }
    }
    }
    }
    return result;
    }
    </pre>

    |

    最后我们新建一个最关键的方法:parseImageDataFromNSDictionary,生成图片空白的占位符,并且设置其CTRunDelegate信息。其代码如下:

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">
    static CGFloat ascentCallback(void ref){
    return [(NSNumber
    )[(__bridge NSDictionary*)ref objectForKey:@"height"] floatValue];
    }

    static CGFloat descentCallback(void *ref){
    return 0;
    }

    static CGFloat widthCallback(void* ref){
    return [(NSNumber)[(__bridge NSDictionary)ref objectForKey:@"width"] floatValue];
    }

    • (NSAttributedString *)parseImageDataFromNSDictionary:(NSDictionary )dict
      config:(CTFrameParserConfig
      )config {
      CTRunDelegateCallbacks callbacks;
      memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
      callbacks.version = kCTRunDelegateVersion1;
      callbacks.getAscent = ascentCallback;
      callbacks.getDescent = descentCallback;
      callbacks.getWidth = widthCallback;
      CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)(dict));

    // 使用 0xFFFC 作为空白的占位符
    unichar objectReplacementChar = 0xFFFC;
    NSString * content = [NSString stringWithCharacters:&objectReplacementChar length:1];
    NSDictionary * attributes = [self attributesWithConfig:config];
    NSMutableAttributedString * space =
    [[NSMutableAttributedString alloc] initWithString:content
    attributes:attributes];
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space,
    CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
    CFRelease(delegate);
    return space;
    }
    </pre>

    |

    接着我们对CoreTextData进行改造,增加了imageArray成员变量,用于保存图片绘制时所需的信息。

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">#import <Foundation/Foundation.h>

    import "CoreTextImageData.h"

    @interface CoreTextData : NSObject

    @property (assign, nonatomic) CTFrameRef ctFrame;
    @property (assign, nonatomic) CGFloat height;
    // 新增加的成员
    @property (strong, nonatomic) NSArray * imageArray;

    @end
    </pre>

    |

    在设置imageArray成员时,我们还会调一个新创建的fillImagePosition方法,用于找到每张图片在绘制时的位置。

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">

    • (void)setImageArray:(NSArray *)imageArray {
      _imageArray = imageArray;
      [self fillImagePosition];
      }

    • (void)fillImagePosition {
      if (self.imageArray.count == 0) {
      return;
      }
      NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
      int lineCount = [lines count];
      CGPoint lineOrigins[lineCount];
      CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);

    int imgIndex = 0;
    CoreTextImageData * imageData = self.imageArray[0];

    for (int i = 0; i < lineCount; ++i) {
    if (imageData == nil) {
    break;
    }
    CTLineRef line = (__bridge CTLineRef)lines[I];
    NSArray * runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
    for (id runObj in runObjArray) {
    CTRunRef run = (__bridge CTRunRef)runObj;
    NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
    CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
    if (delegate == nil) {
    continue;
    }

    NSDictionary * metaDic = CTRunDelegateGetRefCon(delegate);
    if (![metaDic isKindOfClass:[NSDictionary class]]) {
    continue;
    }

    CGRect runBounds;
    CGFloat ascent;
    CGFloat descent;
    runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
    runBounds.size.height = ascent + descent;

    CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
    runBounds.origin.x = lineOrigins[i].x + xOffset;
    runBounds.origin.y = lineOrigins[i].y;
    runBounds.origin.y -= descent;

    CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
    CGRect colRect = CGPathGetBoundingBox(pathRef);

    CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);

    imageData.imagePosition = delegateBounds;
    imgIndex++;
    if (imgIndex == self.imageArray.count) {
    imageData = nil;
    break;
    } else {
    imageData = self.imageArray[imgIndex];
    }
    }
    }
    }
    </pre>

    |

    添加对图片的点击支持

    实现方式

    为了实现对图片的点击支持,我们需要给CTDisplayView类增加用户点击操作的检测函数,在检测函数中,判断当前用户点击的区域是否在图片上,如果在图片上,则触发点击图片的逻辑。苹果提供的UITapGestureRecognizer可以很好的满足我们的要求,所以我们这里用它来检测用户的点击操作。

    我们这里实现的是点击图片后,先用NSLog打印出一行日志。实际应用中,读者可以根据业务需求自行调整点击后的效果。

    我们先为CTDisplayView类增加UITapGestureRecognizer

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
    [self setupEvents];
    }
    return self;
    }

    • (void)setupEvents {
      UIGestureRecognizer * tapRecognizer =
      [[UITapGestureRecognizer alloc] initWithTarget:self
      action:@selector(userTapGestureDetected:)];
      tapRecognizer.delegate = self;
      [self addGestureRecognizer:tapRecognizer];
      self.userInteractionEnabled = YES;
      }
      </pre>

    |

    然后增加UITapGestureRecognizer的回调函数:

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">- (void)userTapGestureDetected:(UIGestureRecognizer *)recognizer {
    CGPoint point = [recognizer locationInView:self];
    for (CoreTextImageData * imageData in self.data.imageArray) {
    // 翻转坐标系,因为 imageData 中的坐标是 CoreText 的坐标系
    CGRect imageRect = imageData.imagePosition;
    CGPoint imagePosition = imageRect.origin;
    imagePosition.y = self.bounds.size.height - imageRect.origin.y

    • imageRect.size.height;
      CGRect rect = CGRectMake(imagePosition.x, imagePosition.y, imageRect.size.width, imageRect.size.height);
      // 检测点击位置 Point 是否在 rect 之内
      if (CGRectContainsPoint(rect, point)) {
      // 在这里处理点击后的逻辑
      NSLog(@"bingo");
      break;
      }
      }
      }
      </pre>

    |

    事件处理

    在界面上,CTDisplayView通常在UIView的树形层级结构中,一个 UIView 可能是最外层 View Controller 的 View 的孩子的孩子的孩子(如下图所示)。在这种多级层次结构中,很难通过delegate模式将图片点击的事件一层一层往外层传递,所以最好使用NSNotification,来处理图片点击事件。

    [图片上传失败...(image-a5937f-1612149797748)]

    在 Demo 中,我们在最外层的 View Controller 中监听图片点击的通知,当收到通知后,进入到一个新的界面来显示图片点击内容。

    注:读者可以将 demo 工程切换到image_click分支,查看示例代码。

    添加对链接的点击支持

    修改模板文件

    我们修改模版文件,增加一个名为 link 的类型,用于表示链接内容。如下所示:

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">[
    { "color" : "default",
    "content" : " 这在这里尝试放一个参考链接:",
    "type" : "txt"
    },
    { "color" : "blue",
    "content" : " 链接文字 ",
    "url" : "http://blog.devtang.com",
    "type" : "link"
    },
    { "color" : "default",
    "content" : " 大家可以尝试点击一下 ",
    "type" : "txt"
    }
    ]
    </pre>

    |

    解析模版中的链接信息

    我们首先增加一个CoreTextLinkData类,用于记录解析 JSON 文件时的链接信息:

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">@interface CoreTextLinkData : NSObject

    @property (strong, nonatomic) NSString * title;
    @property (strong, nonatomic) NSString * url;
    @property (assign, nonatomic) NSRange range;

    @end
    </pre>

    |

    然后我们修改 CTFrameParser 类,增加解析链接的逻辑:

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">+ (NSAttributedString *)loadTemplateFile:(NSString )path
    config:(CTFrameParserConfig
    )config
    imageArray:(NSMutableArray *)imageArray
    linkArray:(NSMutableArray *)linkArray {
    NSData *data = [NSData dataWithContentsOfFile:path];
    NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
    if (data) {
    NSArray *array = [NSJSONSerialization JSONObjectWithData:data
    options:NSJSONReadingAllowFragments
    error:nil];
    if ([array isKindOfClass:[NSArray class]]) {
    for (NSDictionary *dict in array) {
    NSString *type = dict[@"type"];
    if ([type isEqualToString:@"txt"]) {
    // 省略
    } else if ([type isEqualToString:@"img"]) {
    // 省略
    } else if ([type isEqualToString:@"link"]) {
    NSUInteger startPos = result.length;
    NSAttributedString *as =
    [self parseAttributedContentFromNSDictionary:dict
    config:config];
    [result appendAttributedString:as];
    // 创建 CoreTextLinkData
    NSUInteger length = result.length - startPos;
    NSRange linkRange = NSMakeRange(startPos, length);
    CoreTextLinkData *linkData = [[CoreTextLinkData alloc] init];
    linkData.title = dict[@"content"];
    linkData.url = dict[@"url"];
    linkData.range = linkRange;
    [linkArray addObject:linkData];
    }
    }
    }
    }
    return result;
    }
    </pre>

    |

    然后,我们增加一个 Utils 类来专门处理检测用户点击是否在链接上。主要的方法是使用 CTLineGetStringIndexForPosition 函数来获得用户点击的位置与 NSAttributedString 字符串上的位置的对应关系。这样就知道是点击的哪个字符了。然后判断该字符串是否在链接上即可。该 Util 在实现逻辑如下:

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">// 检测点击位置是否在链接上

    • (CoreTextLinkData *)touchLinkInView:(UIView *)view atPoint:(CGPoint)point data:(CoreTextData *)data {
      CTFrameRef textFrame = data.ctFrame;
      CFArrayRef lines = CTFrameGetLines(textFrame);
      if (!lines) return nil;
      CFIndex count = CFArrayGetCount(lines);
      CoreTextLinkData *foundLink = nil;

    // 获得每一行的 origin 坐标
    CGPoint origins[count];
    CTFrameGetLineOrigins(textFrame, CFRangeMake(0,0), origins);

    // 翻转坐标系
    CGAffineTransform transform = CGAffineTransformMakeTranslation(0, view.bounds.size.height);
    transform = CGAffineTransformScale(transform, 1.f, -1.f);

    for (int i = 0; i < count; i++) {
    CGPoint linePoint = origins[I];
    CTLineRef line = CFArrayGetValueAtIndex(lines, i);
    // 获得每一行的 CGRect 信息
    CGRect flippedRect = [self getLineBounds:line point:linePoint];
    CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);

    if (CGRectContainsPoint(rect, point)) {
    // 将点击的坐标转换成相对于当前行的坐标
    CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect),
    point.y-CGRectGetMinY(rect));
    // 获得当前点击坐标对应的字符串偏移
    CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
    // 判断这个偏移是否在我们的链接列表中
    foundLink = [self linkAtIndex:idx linkArray:data.linkArray];
    return foundLink;
    }
    }
    return nil;
    }

    • (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point {
      CGFloat ascent = 0.0f;
      CGFloat descent = 0.0f;
      CGFloat leading = 0.0f;
      CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
      CGFloat height = ascent + descent;
      return CGRectMake(point.x, point.y - descent, width, height);
      }

    • (CoreTextLinkData *)linkAtIndex:(CFIndex)i linkArray:(NSArray *)linkArray {
      CoreTextLinkData *link = nil;
      for (CoreTextLinkData *data in linkArray) {
      if (NSLocationInRange(i, data.range)) {
      link = data;
      break;
      }
      }
      return link;
      }
      </pre>

    |

    最后改造一下CTDisplayView,使其在检测到用户点击后,调用上面的 Util 方法即可。我们这里实现的是点击链接后,先用NSLog打印出一行日志。实际应用中,读者可以根据业务需求自行调整点击后的效果。

    |

    <pre style="font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; overflow: auto; font-size: 0.9rem; margin: 0px; padding: 0px; line-height: 1.3rem; font-weight: 400; color: rgb(171, 178, 191); background-color: rgb(40, 44, 52); border: none;">- (void)userTapGestureDetected:(UIGestureRecognizer *)recognizer {
    CGPoint point = [recognizer locationInView:self];
    // 此处省略上一节中介绍的,对图片点击检测的逻辑

    CoreTextLinkData *linkData = [CoreTextUtils touchLinkInView:self atPoint:point data:self.data];
    if (linkData) {
    NSLog(@"hint link!");
    return;
    }
    }
    </pre>

    |

    注:在 Demo 中工程中,我们实现了点击链接跳转到一个新的界面,然后用 UIWebView 来显示链接内容的逻辑。读者可以将 demo 工程切换到link_click分支,查看示例代码。

    Demo 工程的 Gif 效果图如下,读者可以将示例工程用git checkout image_support切换到当前章节状态,查看相关代码逻辑。

    相关文章

      网友评论

          本文标题:coretext

          本文链接:https://www.haomeiwen.com/subject/fxejtltx.html