Markdownをリアルタイムに解析する

A77456b262557e22986345f6d0555c58?s=47 nakajijapan
August 31, 2018

 Markdownをリアルタイムに解析する

Markdownを解析する時にGitHubで探せば簡単にライブラリを何個も見つけることができます。しかし、をそれをリアルタイムとなるとなかなかみつけることができません。私は端末間で同期できるメモアプリを作成し、機能の一つに入力しながらMarkdown形式に色付けをする機能を実装しました。本トークではリアルタイムに文章を解析し、リッチな表現をどのようにして行っているのかを解説します。

https://fortee.jp/iosdc-japan-2018/proposal/a2e20820-d4f6-43e5-b34b-1b9e6fec7806

A77456b262557e22986345f6d0555c58?s=128

nakajijapan

August 31, 2018
Tweet

Transcript

  1. #iOSDC2018 .BSLEPXOΛ ϦΞϧλΠϜʹ ղੳ͢Δ

  2. #iOSDC2018

  3. !OBLBKJKBQBO 4PGUXBSF&OHJOFFS $".1'*3&J04%$4UB⒎/,+*OUFSOFU J04"OESPJENBD04

  4. None
  5. .BSLEPXOΛ ϦΞϧλΠϜʹ ղੳ͢Δ

  6. .BSLEPXOΛ ϦΞϧλΠϜʹ ղੳ͢Δ ͖͔͚ͬ

  7. Izumo Markdown Note Tool

  8. J04 NBD04

  9. ͖͔͚ͬ wࣗ෼ʹͱͬͯྑ͍ΤσΟλ͕ͳ͔ͬͨ wϦονͷදݱํ๏ʹෆຬ w৭͚ͩมΘͬͯ͘ΕΕ͹͍͍ wಉظͯ͠ཉ͍͠

  10. ΍Γ͍ͨ͜ͱ

  11. wΧϥʔϦϯά wฤूิࠤ

  12. ΧϥʔϦϯά ߦ໨͸λΠτϧߦɺݟग़͠ɺڧௐɺΠϯ ϥΠϯදࣔɺϦετɺ50%0Ϧετɺը૾ ଧͪফ͠ɺςʔϒϧɺਫฏઢɺҾ༻ɺίʔ υϒϩοΫɺҾ༻

  13. ฤूิࠤ wࣗಈΠϯσϯτ wλϒͰͷϦετҠಈ

  14. ௐࠪ

  15. IUUQTHJUIVCDPN TFBSDI RNBSLEPXO TXJGU

  16. None
  17. wϦονͳදݱʹม׵ͯ͠͠·͏ wશจΛಡΈࠐΉඞཁ͕͋Γ

  18. ࠷খݶͷൣғͰղੳ ॲཧΛߦ͍͍ͨ

  19. w͍ͭʁ wԿΛʁ wͲͷΑ͏ʹͯ͠ʁ

  20. ͍ͭʁ

  21. wςΩετ͕ೖྗ͞Εͨ wॳظԽ͞Εͨ wϑΥϯτ͕มߋ͞Εͨ

  22. wςΩετ͕ೖྗ͞Εͨ wॳظԽ͞Εͨ wϑΥϯτ͕มߋ͞Εͨ

  23. /45FYU4UPSBHF%FMFHBUF

  24. // MARK: - NSTextStorageDelegate extension TextViewController: NSTextStorageDelegate {  func

    textStorage(_ textStorage: NSTextStorage, willProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) { self.editedRange = editedRange } func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) {  } }
  25. ԿΛʁ

  26. ม׵͢Δ΂͖ྖҬ

  27. // MARK: - NSTextStorageDelegate extension TextViewController: NSTextStorageDelegate {  func

    textStorage(_ textStorage: NSTextStorage, willProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) { self.editedRange = editedRange } func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) {  } }
  28. // MARK: - NSTextStorageDelegate extension TextViewController: NSTextStorageDelegate {  func

    textStorage(_ textStorage: NSTextStorage, willProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) { self.editedRange = editedRange } func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) {  } } ⚠ฤूͨ͠ྖҬ
  29. લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩ

  30. લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩʂ

  31. લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩ

  32. લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩʂ ໌೔΋J04%$ͩʂ ໌ޙ೔΋J04%$ͩ

  33. Ұؾʹෳ਺ߦ͕ ฤू͞Εͨ৔߹ Λߟྀ͢Δ

  34. ͲͷΑ͏ʹͯ͠ʁ

  35. ͲͷΑ͏ʹͯ͠ʁ wฤूͨ͠ൣғΛऔಘ͢Δ wߦຖʹ෼ׂ͢Δ wߦͷશͯͷൣғΛऔಘ͢Δ wม׵ wશମͷஔ׵

  36. ͲͷΑ͏ʹͯ͠ʁ wฤूͨ͠ൣғΛऔಘ͢Δ wߦຖʹ෼ׂ͢Δ wߦͷશͯͷൣғΛऔಘ͢Δ wม׵ wશମͷஔ׵ ෳ਺ߦͷ৔߹͕͋Δ

  37. ͲͷΑ͏ʹͯ͠ʁ wฤूͨ͠ൣғΛऔಘ͢Δ wߦຖʹ෼ׂ͢Δ wߦͷશͯͷൣғΛऔಘ͢Δ wม׵ wશମͷஔ׵

  38.  J04%$ J04%$

  39. લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩ

  40. લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩʂ

  41. લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩʂ

  42. લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩʂ ໌೔΋J04%$ͩʂ ໌ޙ೔΋J04%$ͩ

  43. લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩʂ ໌೔΋J04%$ͩʂ ໌ޙ೔΋J04%$ͩ

  44. func rangeOfCurrentLine(location: Int) -> NSRange? { let string = internalTextStorage.string

    let endNSRange = NSRange(location: location, length: 0) let lineRange: Range<String.Index> if let endRange = endNSRange.range(string: string) { lineRange = string.lineRange(for: endRange) } else { return nil } let lower = String.UTF16View.Index(lineRange.lowerBound, within: string.utf16)! let upper = String.UTF16View.Index(lineRange.upperBound, within: string.utf16)! let lineNSRange = NSRange(lower..<upper, in: string) return lineNSRange }
  45. func rangeOfCurrentLine(location: Int) -> NSRange? { let string = internalTextStorage.string

    let endNSRange = NSRange(location: location, length: 0) let lineRange: Range<String.Index> if let endRange = endNSRange.range(string: string) { lineRange = string.lineRange(for: endRange) } else { return nil } let lower = String.UTF16View.Index(lineRange.lowerBound, within: string.utf16)! let upper = String.UTF16View.Index(lineRange.upperBound, within: string.utf16)! let lineNSRange = NSRange(lower..<upper, in: string) return lineNSRange }
  46. func rangeOfCurrentLine(location: Int) -> NSRange? { let string = internalTextStorage.string

    let endNSRange = NSRange(location: location, length: 0) let lineRange: Range<String.Index> if let endRange = endNSRange.range(string: string) { lineRange = string.lineRange(for: endRange) } else { return nil } let lower = String.UTF16View.Index(lineRange.lowerBound, within: string.utf16)! let upper = String.UTF16View.Index(lineRange.upperBound, within: string.utf16)! let lineNSRange = NSRange(lower..<upper, in: string) return lineNSRange } վߦͰ෼ׂͨ͠ߦͷ࠷ॳͷҐஔΛऔಘ
  47. func rangeOfCurrentLine(location: Int) -> NSRange? { let string = internalTextStorage.string

    let endNSRange = NSRange(location: location, length: 0) let lineRange: Range<String.Index> if let endRange = endNSRange.range(string: string) { lineRange = string.lineRange(for: endRange) } else { return nil } let lower = String.UTF16View.Index(lineRange.lowerBound, within: string.utf16)! let upper = String.UTF16View.Index(lineRange.upperBound, within: string.utf16)! let lineNSRange = NSRange(lower..<upper, in: string) return lineNSRange } վߦͰ෼ׂͨ͠ߦͷ࠷ॳͷҐஔΛऔಘ લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩʂ ໌೔΋J04%$ͩʂ ໌ޙ೔΋J04%$ͩ
  48. func rangeOfCurrentLine(location: Int) -> NSRange? { let string = internalTextStorage.string

    let endNSRange = NSRange(location: location, length: 0) let lineRange: Range<String.Index> if let endRange = endNSRange.range(string: string) { lineRange = string.lineRange(for: endRange) } else { return nil } let lower = String.UTF16View.Index(lineRange.lowerBound, within: string.utf16)! let upper = String.UTF16View.Index(lineRange.upperBound, within: string.utf16)! let lineNSRange = NSRange(lower..<upper, in: string) return lineNSRange } ର৅ͷߦશମΛऔಘ վߦͰ෼ׂͨ͠ߦͷ࠷ॳͷҐஔΛऔಘ
  49. func rangeOfCurrentLine(location: Int) -> NSRange? { let string = internalTextStorage.string

    let endNSRange = NSRange(location: location, length: 0) let lineRange: Range<String.Index> if let endRange = endNSRange.range(string: string) { lineRange = string.lineRange(for: endRange) } else { return nil } let lower = String.UTF16View.Index(lineRange.lowerBound, within: string.utf16)! let upper = String.UTF16View.Index(lineRange.upperBound, within: string.utf16)! let lineNSRange = NSRange(lower..<upper, in: string) return lineNSRange } ର৅ͷߦશମΛऔಘ վߦͰ෼ׂͨ͠ߦͷ࠷ॳͷҐஔΛऔಘ લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩʂ ໌೔΋J04%$ͩʂ ໌ޙ೔΋J04%$ͩ
  50. lineRange(for:) Returns the range of characters representing the line or

    lines containing a given range. func lineRange(for range: NSRange) -> NSRange ศ ར
  51. ͲͷΑ͏ʹͯ͠ʁ wฤूͨ͠ൣғΛऔಘ͢Δ wߦຖʹ෼ׂ͢Δ wߦͷશͯͷൣғΛऔಘ͢Δ wม׵ wશମͷஔ׵

  52. ͲͷΑ͏ʹͯ͠ʁ wฤूͨ͠ൣғΛऔಘ͢Δ wߦຖʹ෼ׂ͢Δ wߦͷશͯͷൣғΛऔಘ͢Δ wม׵ wશମͷஔ׵ ίʔυϒϩοΫ͸શ ମΛݕࡧඞཁ͕͋Δ

  53. ͲͷΑ͏ʹͯ͠ʁ wฤूͨ͠ൣғΛऔಘ͢Δ wߦຖʹ෼ׂ͢Δ wߦͷશͯͷൣғΛऔಘ͢Δ wม׵ wશମͷஔ׵ ޙ͸֘౰͢ΔจࣈྻΛந ग़ͯ͠ద੾ͳ৭ʹม׵͢ Δ͚ͩɻ

  54. ࠷খݶͷൣғͰղ ੳ͢Δ͜ͱ͕େ੾

  55. None
  56. #$%&

  57. ֆจࣈ w/44USJOHɺ/43BOHFͷར༻࣌ w୯७ʹจࣈྻΛΧ΢ϯτͯ͠͸❌ w65'Λҙࣝͨ͠จࣈྻૢ࡞Λ͢Δ

  58. None
  59. 6 ' Y%% Y%& 6OJDPEF 65'

  60. "཮".count // 1 "཮".unicodeScalars.count // 1 "཮".utf16.count // 1 "཮".utf8.count

    // 3 "".count // 1 "".unicodeScalars.count // 1 "".utf16.count // 2 "".utf8.count // 4 ")".count // 1 ")".unicodeScalars.count // 2 ")".utf16.count // 4 ")".utf8.count // 8
  61. #$%&

  62. /44USJOH

  63. w಺෦తʹ͸65'Ͱό ΠτྻΛ؅ཧ /44USJOH

  64. let snowy = "❄ Let it snow! ☃" let nsrange

    = NSRange(location: 3, length: 12) if let range = Range(nsrange, in: snowy) { print(snowy[range]) } // Prints "Let it snow!"
  65. NSRange(location: 0, length: line.utf16.count)

  66. 4USJOH

  67. wόΠτྻ͸Ӆณ 4USJOH

  68. let snowy = "❄ Let it snow! ☃" print(snowy.prefix(5)) //

    Prints "❄ Let" print(snowy.suffix(1)) // Prints "☃"
  69. None
  70. ೖྗิࠤ

  71. wΦʔτΠϯσϯτ wλϒͰͷҠಈ

  72. None
  73. ΠϕϯτϋϯυϦϯά wλϒ wόοΫλϒ wվߦ

  74. J04NBD04

  75. w/45FYU7JFX w6*5FYU7JFX

  76. /45FYU7JFX

  77. None
  78. ઐ༻ͷؔ਺ ͕༻ҙ͞Ε͍ͯΔ

  79. class NSTextView { func insertTab(_ sender: Any?) func insertBacktab(_ sender:

    Any?) func insertNewline(_ sender: Any?) } public protocol NSTextInputClient { public func insertText(_ string: Any, replacementRange: NSRange) } طʹظ଴͞Εͨؔ਺͕༻ҙ͞Ε͍ͯΔ ΧελϚΠζͨ͠จࣈྻΛૠೖ͢Δ
  80. class TextView: NS { override func insertTab(_ sender: Any?) {

    var indent = "" let (spaces, lineRange) = getPrefixStringIfLineHasListTag(regex: TextRegex.insertTab) if useIndentWithSpaceKey { indent = String(repeating: " ", count: indentWidth) if !spaces.isEmpty { super.insertText(indent, replacementRange: NSRange(location: lineRange.location, length: 0)) return } super.insertText(indent, replacementRange: self.rangeForUserTextChange) return } indent = "\t" if !spaces.isEmpty { super.insertText(indent, replacementRange: NSRange(location: lineRange.location, length: 0)) return } super.insertTab(sender) } }
  81. class TextView: NS { override func insertTab(_ sender: Any?) {

    var indent = "" let (spaces, lineRange) = getPrefixStringIfLineHasListTag(regex: TextRegex.insertTab) if useIndentWithSpaceKey { indent = String(repeating: " ", count: indentWidth) if !spaces.isEmpty { super.insertText(indent, replacementRange: NSRange(location: lineRange.location, length: 0)) return } super.insertText(indent, replacementRange: self.rangeForUserTextChange) return } indent = "\t" if !spaces.isEmpty { super.insertText(indent, replacementRange: NSRange(location: lineRange.location, length: 0)) return } super.insertTab(sender) } } Πϯσϯτର৅ͱͳ Δઌ಄෦෼Λऔಘ
  82.  લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩ  લ೔͸લ໷ࡇͩͬͨɻ  ࠓ೔͸J04%$ͩ

  83. class TextView: NS { override func insertTab(_ sender: Any?) {

    var indent = "" let (spaces, lineRange) = getPrefixStringIfLineHasListTag(regex: TextRegex.insertTab) if useIndentWithSpaceKey { indent = String(repeating: " ", count: indentWidth) if !spaces.isEmpty { super.insertText(indent, replacementRange: NSRange(location: lineRange.location, length: 0)) return } super.insertText(indent, replacementRange: self.rangeForUserTextChange) return } indent = "\t" if !spaces.isEmpty { super.insertText(indent, replacementRange: NSRange(location: lineRange.location, length: 0)) return } super.insertTab(sender) } } ਌ॲཧʹ೚ͤΔ
  84. 6*5FYU7JFX

  85. None
  86. class NSTextView { func insertTab(_ sender: Any?) func insertBacktab(_ sender:

    Any?) func insertNewline(_ sender: Any?) } public protocol NSTextInputClient { public func insertText(_ string: Any, replacementRange: NSRange) } ༻ҙ͞Ε͍ͯͳ͍ɻɻɻ
  87. class NSTextView { func insertTab(_ sender: Any?) func insertBacktab(_ sender:

    Any?) func insertNewline(_ sender: Any?) } public protocol NSTextInputClient { public func insertText(_ string: Any, replacementRange: NSRange) } • ΩʔϘʔυʹλϒ͸ແ͍ • λϒϘλϯͰಉ౳ͷػೳΛ࣮૷
  88. insertTab insertBackTab

  89. ςΩετૠೖ࣌

  90. 6*5FYU7JFXͷࢠΫϥε let stringRange = lineRange.range(string: text)! text.insert(contentsOf: indent, at: stringRange.lowerBound)

  91. 6*5FYU7JFXͷࢠΫϥε let stringRange = lineRange.range(string: text)! text.insert(contentsOf: indent, at: stringRange.lowerBound)

  92. UITextViewͷࢠΫϥε let stringRange = lineRange.range(string: text)! text.insert(contentsOf: indent, at: stringRange.lowerBound)

    wFEJUFE3BOHFͰฤूൣғ͕શͯʹͳͬ ͯ͠·͏ wUFYU%JE$IBOHF͸ίʔϧ͞Εͳ͍ wظ଴͞ΕͨҐஔʹΧʔιϧ͕Ҡಈ͞Ε ͳ͍
  93. public protocol UIKeyInput : UITextInputTraits { public var hasText: Bool

    { get } public func insertText(_ text: String) public func deleteBackward() }
  94. func insertTab(rangeForTextChange: NSRange) { var indent = "" let (spaces,

    lineRange) = getPrefixStringIfLineHasList(regex: TextInputRegex.insertTab, rangeForTextChange: rangeForTextChange) if useIndentWithSpaceKey { indent = String(repeating: " ", count: indentWidth) if !spaces.isEmpty { selectedRange = NSRange(location: lineRange.location, length: 0) insertText(indent) selectedRange = NSRange(location: rangeForTextChange.location + indentWidth, length: 0) return } super.insertText(indent) return } indent = "\t" if !spaces.isEmpty { selectedRange = NSRange(location: lineRange.location, length: 0) insertText(indent) selectedRange = NSRange(location: rangeForTextChange.location + 1, length: 0) return } }
  95. func insertTab(rangeForTextChange: NSRange) { var indent = "" let (spaces,

    lineRange) = getPrefixStringIfLineHasList(regex: TextInputRegex.insertTab, rangeForTextChange: rangeForTextChange) if useIndentWithSpaceKey { indent = String(repeating: " ", count: indentWidth) if !spaces.isEmpty { selectedRange = NSRange(location: lineRange.location, length: 0) insertText(indent) selectedRange = NSRange(location: rangeForTextChange.location + indentWidth, length: 0) return } super.insertText(indent) return } indent = "\t" if !spaces.isEmpty { selectedRange = NSRange(location: lineRange.location, length: 0) insertText(indent) selectedRange = NSRange(location: rangeForTextChange.location + 1, length: 0) return } } Χʔιϧ͕ظ଴ͨ͠Ґஔʹ͍ͳ͍ͷͰઌ಄ʹҠಈɺૠೖޙɺ ૠೖޙͷҐஔʹҠಈ͢Δඞཁ͕͋Δɻ
  96. ·ͱΊ

  97. ·ͱΊ wղੳ͸ͳΔ΂͘࠷খݶͷൣғͰղੳ͢Δ

  98. ·ͱΊ wղੳ͸ͳΔ΂͘࠷খݶͷൣғͰߦ͏ wจࣈίʔυΛҙࣝͨ͠จࣈྻૢ࡞ w⚠/44USJOH /43BOHF

  99. ·ͱΊ wղੳ͸ͳΔ΂͘࠷খݶͷൣғͰߦ͏ wจࣈίʔυΛҙࣝͨ͠จࣈྻૢ࡞ w⚠/44USJOH /43BOHF wೖྗิࠤ࣌ͷJ04Ͱͷઃܭ wΧελϚΠζ͕ඞਢ

  100. 5IBOLT

  101. &OKPZ