AttributedString.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.Xml;
  5. using CommonLang;
  6. using CommonUI.Cell.Game;
  7. using CommonUI.Data;
  8. using CommonLang.Log;
  9. using CommonLang.Xml;
  10. namespace CommonUI.Display.Text
  11. {
  12. /// <summary>
  13. /// 文字字符上的属性
  14. /// </summary>
  15. public class TextAttribute : ICloneable
  16. {
  17. /// <summary>
  18. /// 字符颜色 0 表示无特性 (RGBA)
  19. /// </summary>
  20. public uint fontColor = 0xffffffff;
  21. /// <summary>
  22. /// 子尺寸 0 表示无特性
  23. /// </summary>
  24. public float fontSize;
  25. /// <summary>
  26. /// 字体名字,空表示无特性
  27. /// </summary>
  28. public string fontName;
  29. /// <summary>
  30. /// 字体
  31. /// </summary>
  32. public FontStyle fontStyle = FontStyle.STYLE_PLAIN;
  33. public bool underline
  34. {
  35. get
  36. {
  37. switch (fontStyle)
  38. {
  39. case FontStyle.STYLE_BOLD_ITALIC_UNDERLINED:
  40. case FontStyle.STYLE_ITALIC_UNDERLINED:
  41. case FontStyle.STYLE_BOLD_UNDERLINED:
  42. case FontStyle.STYLE_UNDERLINED:
  43. return true;
  44. }
  45. return false;
  46. }
  47. set
  48. {
  49. if (!value)
  50. {
  51. switch (fontStyle)
  52. {
  53. case FontStyle.STYLE_BOLD_ITALIC_UNDERLINED:
  54. fontStyle = FontStyle.STYLE_BOLD_ITALIC;
  55. break;
  56. case FontStyle.STYLE_ITALIC_UNDERLINED:
  57. fontStyle = FontStyle.STYLE_ITALIC;
  58. break;
  59. case FontStyle.STYLE_BOLD_UNDERLINED:
  60. fontStyle = FontStyle.STYLE_BOLD;
  61. break;
  62. case FontStyle.STYLE_UNDERLINED:
  63. fontStyle = FontStyle.STYLE_PLAIN;
  64. break;
  65. }
  66. }
  67. else
  68. {
  69. switch (fontStyle)
  70. {
  71. case FontStyle.STYLE_BOLD_ITALIC:
  72. fontStyle = FontStyle.STYLE_BOLD_ITALIC_UNDERLINED;
  73. break;
  74. case FontStyle.STYLE_ITALIC:
  75. fontStyle = FontStyle.STYLE_ITALIC_UNDERLINED;
  76. break;
  77. case FontStyle.STYLE_BOLD:
  78. fontStyle = FontStyle.STYLE_BOLD_UNDERLINED;
  79. break;
  80. case FontStyle.STYLE_PLAIN:
  81. fontStyle = FontStyle.STYLE_UNDERLINED;
  82. break;
  83. }
  84. }
  85. }
  86. }
  87. /// <summary>
  88. /// 描边
  89. /// </summary>
  90. public TextBorderCount borderCount = TextBorderCount.Null;
  91. /// <summary>
  92. /// 描边颜色 (RGBA)
  93. /// </summary>
  94. public uint borderColor;
  95. /// <summary>
  96. /// 此字符替换成图片,空表示无特性
  97. /// </summary>
  98. public string resImage;
  99. /// <summary>
  100. /// 图片渲染方式
  101. /// </summary>
  102. public ImageZoom resImageZoom;
  103. /// <summary>
  104. /// 此字符替换成动画,空表示无特性
  105. /// </summary>
  106. public string resSprite;
  107. /// <summary>
  108. /// 标记此处可以被点击触发事件,空表示无特性
  109. /// </summary>
  110. public string link;
  111. /// <summary>
  112. /// 行对齐方式
  113. /// </summary>
  114. public RichTextAlignment anchor = RichTextAlignment.taNA;
  115. /// <summary>
  116. /// 扩展的自定义渲染部分
  117. /// </summary>
  118. public TextDrawable drawable;
  119. public TextAttribute(
  120. uint fColor = 0,
  121. float fSize = 0,
  122. string fName = null,
  123. FontStyle fStyle = FontStyle.STYLE_PLAIN,
  124. RichTextAlignment ta = RichTextAlignment.taNA,
  125. TextBorderCount bCount = TextBorderCount.Null,
  126. uint bColor = 0,
  127. string rImage = null,
  128. string rSprite = null,
  129. string pLink = null,
  130. ImageZoom rImageZoom = null,
  131. TextDrawable drawable = null)
  132. {
  133. this.fontColor = fColor;
  134. this.fontSize = fSize;
  135. this.fontName = fName;
  136. this.fontStyle = fStyle;
  137. this.anchor = ta;
  138. this.resImage = rImage;
  139. this.resImageZoom = rImageZoom;
  140. this.resSprite = rSprite;
  141. this.borderCount = bCount;
  142. this.borderColor = bColor;
  143. this.link = pLink;
  144. this.drawable = drawable;
  145. }
  146. public TextAttribute(TextAttribute other)
  147. {
  148. this.fontColor = other.fontColor;
  149. this.fontSize = other.fontSize;
  150. this.fontName = other.fontName;
  151. this.fontStyle = other.fontStyle;
  152. this.anchor = other.anchor;
  153. this.borderCount = other.borderCount;
  154. this.borderColor = other.borderColor;
  155. this.resImage = other.resImage;
  156. this.resImageZoom = CUtils.TryClone(other.resImageZoom);
  157. this.resSprite = other.resSprite;
  158. this.link = other.link;
  159. this.drawable = other.drawable;
  160. }
  161. public object Clone()
  162. {
  163. return new TextAttribute(this);
  164. }
  165. public override bool Equals(object obj)
  166. {
  167. if (obj is TextAttribute)
  168. {
  169. return this.Equals(obj as TextAttribute);
  170. }
  171. return false;
  172. }
  173. public override int GetHashCode()
  174. {
  175. return base.GetHashCode();
  176. }
  177. public bool Equals(TextAttribute other)
  178. {
  179. if (other == this)
  180. return true;
  181. if (other == null)
  182. return false;
  183. if (this.fontColor != other.fontColor)
  184. return false;
  185. if (this.fontSize != other.fontSize)
  186. return false;
  187. if (!string.Equals(this.fontName, other.fontName))
  188. return false;
  189. if (this.fontStyle != other.fontStyle)
  190. return false;
  191. if (this.anchor != other.anchor)
  192. return false;
  193. if (this.borderCount != other.borderCount)
  194. return false;
  195. if (this.borderColor != other.borderColor)
  196. return false;
  197. if (!string.Equals(this.resImage, other.resImage))
  198. return false;
  199. if (!string.Equals(this.resSprite, other.resSprite))
  200. return false;
  201. if (!ImageZoom.Equals(this.resImageZoom, other.resImageZoom))
  202. return false;
  203. if (!string.Equals(this.link, other.link))
  204. return false;
  205. if (drawable != null && !drawable.Equals(other.drawable))
  206. return false;
  207. if (drawable == null && other.drawable != null)
  208. {
  209. return false;
  210. }
  211. return true;
  212. }
  213. public bool IsValid()
  214. {
  215. if (fontColor != 0)
  216. return true;
  217. if (fontSize != 0)
  218. return true;
  219. if (!string.IsNullOrEmpty(fontName))
  220. return true;
  221. if (anchor != RichTextAlignment.taNA)
  222. return true;
  223. if (borderCount > 0)
  224. return true;
  225. if (borderColor != 0)
  226. return true;
  227. if (!string.IsNullOrEmpty(resImage))
  228. return true;
  229. if (!string.IsNullOrEmpty(resSprite))
  230. return true;
  231. if (resImageZoom != null)
  232. return true;
  233. if (!string.IsNullOrEmpty(link))
  234. return true;
  235. if (drawable != null)
  236. return true;
  237. return false;
  238. }
  239. public void Combine(TextAttribute other, bool isCover = true)
  240. {
  241. if (other.fontColor != 0)
  242. {
  243. this.fontColor = other.fontColor;
  244. }
  245. if (other.fontSize != 0)
  246. {
  247. this.fontSize = other.fontSize;
  248. }
  249. if (!string.IsNullOrEmpty(other.fontName))
  250. {
  251. this.fontName = other.fontName;
  252. }
  253. if (other.fontStyle != 0)
  254. {
  255. this.fontStyle = other.fontStyle;
  256. }
  257. if (other.anchor != RichTextAlignment.taNA)
  258. {
  259. this.anchor = other.anchor;
  260. }
  261. if (other.borderCount > 0)
  262. {
  263. this.borderCount = other.borderCount;
  264. }
  265. if (other.borderColor != 0)
  266. {
  267. this.borderColor = other.borderColor;
  268. }
  269. if (!string.IsNullOrEmpty(other.resImage))
  270. {
  271. this.resImage = other.resImage;
  272. }
  273. if (!string.IsNullOrEmpty(other.resSprite))
  274. {
  275. this.resSprite = other.resSprite;
  276. }
  277. if (other.resImageZoom != null)
  278. {
  279. this.resImageZoom = CUtils.TryClone(other.resImageZoom);
  280. }
  281. if (other.drawable != null)
  282. {
  283. this.drawable = CUtils.TryClone(other.drawable);
  284. }
  285. if (isCover && !string.IsNullOrEmpty(other.link))
  286. {
  287. this.link = other.link;
  288. }
  289. else if (!isCover)
  290. {
  291. this.link += other.link;
  292. }
  293. }
  294. public static TextAttribute Combine(TextAttribute src, TextAttribute dst)
  295. {
  296. TextAttribute ret = new TextAttribute(src);
  297. ret.Combine(dst);
  298. return ret;
  299. }
  300. }
  301. //------------------------------------------------------------------------------------------------------------------------
  302. //------------------------------------------------------------------------------------------------------------------------
  303. public class ImageZoomParser : CommonLang.Parser.ParserAdapter
  304. {
  305. public ImageZoomParser() : base(typeof(ImageZoom)) { }
  306. public override object StringToObject(string text)
  307. {
  308. return ImageZoom.FromString(text);
  309. }
  310. public override string ObjectToString(object obj)
  311. {
  312. return ImageZoom.ToString(obj as ImageZoom);
  313. }
  314. }
  315. public class ImageZoom : ICloneable
  316. {
  317. static ImageZoom()
  318. {
  319. Parser.RegistParser(new ImageZoomParser());
  320. }
  321. public enum ImageFill
  322. {
  323. Clamp,
  324. Repeat,
  325. }
  326. public ImageFill Filling = ImageFill.Clamp;
  327. public float Width;
  328. public float Height;
  329. public object Clone()
  330. {
  331. ImageZoom ret = new ImageZoom();
  332. ret.Filling = this.Filling;
  333. ret.Width = this.Width;
  334. ret.Height = this.Height;
  335. return ret;
  336. }
  337. public static bool Equals(ImageZoom a, ImageZoom b)
  338. {
  339. if (a != null && b != null)
  340. {
  341. if (a.Filling != b.Filling)
  342. return false;
  343. if (a.Width != b.Width)
  344. return false;
  345. if (a.Height != b.Height)
  346. return false;
  347. return true;
  348. }
  349. return a == b;
  350. }
  351. public override string ToString()
  352. {
  353. return ToString(this);
  354. }
  355. public static ImageZoom FromString(string text)
  356. {
  357. string[] kvs = text.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
  358. if (kvs.Length >= 2)
  359. {
  360. ImageZoom ret = new ImageZoom();
  361. ret.Width = float.Parse(kvs[0]);
  362. ret.Height = float.Parse(kvs[1]);
  363. if (kvs.Length >= 3)
  364. {
  365. ret.Filling = (ImageFill)Enum.Parse(typeof(ImageFill), kvs[2], true);
  366. }
  367. return ret;
  368. }
  369. return null;
  370. }
  371. public static string ToString(ImageZoom obj)
  372. {
  373. if (obj != null)
  374. {
  375. return string.Format("{0},{1},{2}", obj.Width, obj.Height, obj.Filling);
  376. }
  377. return null;
  378. }
  379. }
  380. //------------------------------------------------------------------------------------------------------------------------
  381. /////////////////////////////////////////////////////////////////////////////////////////////
  382. //
  383. /////////////////////////////////////////////////////////////////////////////////////////////
  384. /// <summary>
  385. /// 含有属性的文字
  386. /// </summary>
  387. public class AttributedString : ICloneable
  388. {
  389. private string text = "";
  390. private List<TextAttribute> attributes = new List<TextAttribute>();
  391. public AttributedString()
  392. {
  393. }
  394. public AttributedString(string other, TextAttribute ta)
  395. {
  396. Append(other, ta);
  397. }
  398. public object Clone()
  399. {
  400. AttributedString ret = new AttributedString();
  401. ret.text = this.text;
  402. ret.attributes = CUtils.CloneList<TextAttribute>(this.attributes);
  403. return ret;
  404. }
  405. public override bool Equals(object obj)
  406. {
  407. if (obj is AttributedString)
  408. {
  409. AttributedString other = obj as AttributedString;
  410. if (other.text.Equals(this.text))
  411. {
  412. for (int i = text.Length - 1; i >= 0; --i)
  413. {
  414. if (!other.attributes[i].Equals(this.attributes[i]))
  415. {
  416. return false;
  417. }
  418. }
  419. return true;
  420. }
  421. }
  422. return false;
  423. }
  424. public override int GetHashCode()
  425. {
  426. return base.GetHashCode();
  427. }
  428. public bool SetAttribute(int index, int len, TextAttribute ta)
  429. {
  430. if (index + len <= attributes.Count)
  431. {
  432. int end = index + len;
  433. for (int i = index; i < end; ++i)
  434. {
  435. attributes[i] = ta;
  436. }
  437. return true;
  438. }
  439. return false;
  440. }
  441. public AttributedString AddAttribute(TextAttribute attribute, bool isCover = true)
  442. {
  443. return AddAttribute(attribute, 0, text.Length, isCover);
  444. }
  445. public AttributedString AddAttribute(TextAttribute attribute, int beginIndex, int count, bool isCover = true)
  446. {
  447. int endIndex = beginIndex + count;
  448. for (int i = beginIndex; i < endIndex; ++i)
  449. {
  450. attributes[i].Combine(attribute, isCover);
  451. }
  452. return this;
  453. }
  454. public AttributedString Append(AttributedString other)
  455. {
  456. if (other != null && !string.IsNullOrEmpty(other.text))
  457. {
  458. text += other.text;
  459. for (int i = 0; i < other.text.Length; ++i)
  460. {
  461. attributes.Add(other.attributes[i]);
  462. }
  463. }
  464. return this;
  465. }
  466. public AttributedString Append(string other)
  467. {
  468. if (attributes.Count > 0)
  469. {
  470. TextAttribute lastAttr = attributes[attributes.Count - 1];
  471. Append(other, lastAttr);
  472. }
  473. else
  474. {
  475. Append(other, new TextAttribute(Color.COLOR_WHITE, 12));
  476. }
  477. return this;
  478. }
  479. public AttributedString Append(string other, TextAttribute ta)
  480. {
  481. if (!string.IsNullOrEmpty(other))
  482. {
  483. text += other;
  484. for (int i = 0; i < other.Length; ++i)
  485. {
  486. attributes.Add(ta);
  487. }
  488. }
  489. return this;
  490. }
  491. public AttributedString DeleteString(int beginIndex, int count)
  492. {
  493. text = text.Remove(beginIndex, count);
  494. attributes.RemoveRange(beginIndex, count);
  495. return this;
  496. }
  497. public AttributedString ClearString()
  498. {
  499. text = "";
  500. attributes.Clear();
  501. return this;
  502. }
  503. public int Length
  504. {
  505. get
  506. {
  507. return text.Length;
  508. }
  509. }
  510. override public string ToString()
  511. {
  512. return text;
  513. }
  514. public char GetChar(int index)
  515. {
  516. if (index < text.Length)
  517. {
  518. return text[index];
  519. }
  520. return default(char);
  521. }
  522. public TextAttribute GetAttribute(int index)
  523. {
  524. if (index < attributes.Count)
  525. {
  526. return attributes[index];
  527. }
  528. return null;
  529. }
  530. }
  531. //------------------------------------------------------------------------------------------------------------------------
  532. //------------------------------------------------------------------------------------------------------------------------
  533. public class AttributedStringDecoder
  534. {
  535. protected Logger log = LoggerFactory.GetLogger("AttributedStringDecoder");
  536. public const string TEXT_XML_KEY_COLOR = "color";
  537. public const string TEXT_XML_KEY_SIZE = "size";
  538. public const string TEXT_XML_KEY_FACE = "face";
  539. public const string TEXT_XML_KEY_STYLE = "style";
  540. public const string TEXT_XML_KEY_B_COUNT = "border";
  541. public const string TEXT_XML_KEY_B_COLOR = "bcolor";
  542. public const string TEXT_XML_KEY_RES_IMG = "img";
  543. /// <summary>
  544. /// @"宽,高"
  545. /// img_zoom = "width,height"
  546. /// </summary>
  547. public const string TEXT_XML_KEY_RES_IMAGE_ZOOM = "img_zoom";
  548. /// <summary>
  549. /// @"资源名,精灵名,动画ID"
  550. /// spr = "res/sprite.xml,sprite_name,anim"
  551. /// </summary>
  552. public const string TEXT_XML_KEY_RES_SPR = "spr";
  553. public const string TEXT_XML_KEY_LINK = "link";
  554. public const string TEXT_XML_KEY_LINE_ANCHOR = "anchor";
  555. public const string TEXT_XML_NODE_BREAK = "br";
  556. public const string TEXT_XML_NODE_SPACE = "p";
  557. public virtual AttributedString CreateFromXML(XmlDocument xml, TextAttribute defaultTA = null)
  558. {
  559. AttributedString attr = new AttributedString();
  560. TextAttribute curAttr = (defaultTA != null) ? defaultTA : new TextAttribute(Color.COLOR_WHITE, 16);
  561. try
  562. {
  563. internalBuildXML(attr, xml.DocumentElement, curAttr);
  564. }
  565. catch (Exception err)
  566. {
  567. log.Error(err.Message, err);
  568. }
  569. return attr;
  570. }
  571. public virtual AttributedString CreateFromXML(string text, TextAttribute defaultTA = null)
  572. {
  573. XmlDocument xml = XmlUtil.FromString(text);
  574. return CreateFromXML(xml, defaultTA);
  575. }
  576. protected virtual void DecodeAttribute(XmlElement node, XmlAttribute x_attr, TextAttribute attr)
  577. {
  578. switch (x_attr.Name)
  579. {
  580. case TEXT_XML_KEY_COLOR:
  581. {
  582. string kv = node.GetAttribute(TEXT_XML_KEY_COLOR);
  583. uint argb = uint.Parse(kv, System.Globalization.NumberStyles.HexNumber);
  584. attr.fontColor = Color.toRGBA(argb);
  585. }
  586. break;
  587. case TEXT_XML_KEY_SIZE:
  588. {
  589. attr.fontSize = int.Parse(node.GetAttribute(TEXT_XML_KEY_SIZE));
  590. }
  591. break;
  592. case TEXT_XML_KEY_STYLE:
  593. {
  594. string style = node.GetAttribute(TEXT_XML_KEY_STYLE);
  595. int value;
  596. if (int.TryParse(style, out value))
  597. {
  598. attr.fontStyle = (FontStyle)value;
  599. }
  600. else
  601. {
  602. attr.fontStyle = (FontStyle) Enum.Parse(typeof(FontStyle), style);
  603. }
  604. }
  605. break;
  606. case TEXT_XML_KEY_FACE:
  607. {
  608. attr.fontName = node.GetAttribute(TEXT_XML_KEY_FACE);
  609. }
  610. break;
  611. case TEXT_XML_KEY_B_COUNT:
  612. {
  613. attr.borderCount = (Data.TextBorderCount)int.Parse(node.GetAttribute(TEXT_XML_KEY_B_COUNT));
  614. }
  615. break;
  616. case TEXT_XML_KEY_B_COLOR:
  617. {
  618. string kv = node.GetAttribute(TEXT_XML_KEY_B_COLOR);
  619. uint argb = uint.Parse(kv, System.Globalization.NumberStyles.HexNumber);
  620. attr.borderColor = Color.toRGBA(argb);
  621. }
  622. break;
  623. case TEXT_XML_KEY_RES_IMG:
  624. {
  625. attr.resImage = node.GetAttribute(TEXT_XML_KEY_RES_IMG);
  626. }
  627. break;
  628. case TEXT_XML_KEY_RES_SPR:
  629. {
  630. attr.resSprite = node.GetAttribute(TEXT_XML_KEY_RES_SPR);
  631. }
  632. break;
  633. case TEXT_XML_KEY_RES_IMAGE_ZOOM:
  634. {
  635. attr.resImageZoom = ImageZoom.FromString(node.GetAttribute(TEXT_XML_KEY_RES_IMAGE_ZOOM));
  636. }
  637. break;
  638. case TEXT_XML_KEY_LINK:
  639. {
  640. attr.link = node.GetAttribute(TEXT_XML_KEY_LINK);
  641. }
  642. break;
  643. case TEXT_XML_KEY_LINE_ANCHOR:
  644. {
  645. string value = node.GetAttribute(TEXT_XML_KEY_LINE_ANCHOR);
  646. try
  647. {
  648. attr.anchor = (RichTextAlignment)Enum.Parse(typeof(RichTextAlignment), value, true);
  649. }
  650. catch (Exception err)
  651. {
  652. log.Error(err.Message, err);
  653. }
  654. }
  655. break;
  656. }
  657. TextDrawable drawable = ITextDrawableFactory.Instance.CreateTextDrawable(x_attr.Name, x_attr.Value);
  658. if (drawable != null)
  659. {
  660. attr.drawable = drawable;
  661. }
  662. }
  663. private void internalBuildXML(AttributedString atext, XmlElement node, TextAttribute parentAttr)
  664. {
  665. string nname = node.Name;
  666. TextAttribute attr = new TextAttribute();
  667. foreach (XmlAttribute x_attr in node.Attributes)
  668. {
  669. DecodeAttribute(node, x_attr, attr);
  670. }
  671. attr = TextAttribute.Combine(parentAttr, attr);
  672. if (!attr.IsValid())
  673. {
  674. attr = parentAttr;
  675. }
  676. if (node.ChildNodes.Count > 0)
  677. {
  678. foreach (XmlNode e in node.ChildNodes)
  679. {
  680. if (e is XmlText || e is XmlCDataSection)
  681. {
  682. atext.Append(e.Value, attr);
  683. }
  684. else if (e is XmlElement)
  685. {
  686. internalBuildXML(atext, e as XmlElement, attr);
  687. }
  688. }
  689. }
  690. if (string.Equals(nname, TEXT_XML_NODE_BREAK))
  691. {
  692. atext.Append("\n", attr);
  693. }
  694. else if (string.Equals(nname, TEXT_XML_NODE_SPACE))
  695. {
  696. atext.Append(" ", attr);
  697. }
  698. }
  699. }
  700. }