182 lines
6.8 KiB
Kotlin
182 lines
6.8 KiB
Kotlin
package jp.juggler.subwaytooter.emoji
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Context
|
|
import jp.juggler.util.log.LogCategory
|
|
import jp.juggler.util.log.errorEx
|
|
import java.io.EOFException
|
|
import java.io.InputStream
|
|
|
|
class EmojiMapLoader(
|
|
appContext: Context,
|
|
private val dst: EmojiMap,
|
|
) {
|
|
// このクラスは起動時に1回だけ使うため、companion objectに永続的に何か保持することはない
|
|
private val log = LogCategory("EmojiMapLoader")
|
|
private val reComment = """\s*//.*""".toRegex()
|
|
private val reLineHeader = """\A(\w+):""".toRegex()
|
|
|
|
private val packageName = appContext.packageName!!
|
|
private val assetsSet = appContext.assets.list("")!!.toSet()
|
|
private val resources = appContext.resources!!
|
|
|
|
private val categoryNameMap = EmojiCategory.entries.associateBy { it.name }
|
|
|
|
private var lastEmoji: UnicodeEmoji? = null
|
|
private var lastCategory: EmojiCategory? = null
|
|
|
|
@SuppressLint("DiscouragedApi")
|
|
private fun getDrawableId(name: String) =
|
|
resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 }
|
|
|
|
// 素の数字とcopyright,registered, trademark は絵文字にしない
|
|
private fun isIgnored(code: String): Boolean {
|
|
val c = code[0].code
|
|
return code.length == 1 && c <= 0xae
|
|
}
|
|
|
|
private fun addCode(emoji: UnicodeEmoji, code: String) {
|
|
if (isIgnored(code)) return
|
|
dst.unicodeMap[code] = emoji
|
|
dst.unicodeTrie.append(code, 0, emoji)
|
|
}
|
|
|
|
private fun addName(emoji: UnicodeEmoji, name: String) {
|
|
dst.shortNameMap[name] = emoji
|
|
dst.shortNameList.add(name)
|
|
emoji.namesLower.add(name.lowercase())
|
|
}
|
|
|
|
private fun readEmojiDataLine(lno: Int, rawLine: String) {
|
|
var line = rawLine.replace(reComment, "").trim()
|
|
val head = reLineHeader.find(line)?.groupValues?.elementAtOrNull(1)
|
|
?: error("missing line header. line=$lno $line")
|
|
line = line.substring(head.length + 1)
|
|
try {
|
|
when (head) {
|
|
"svg" -> {
|
|
if (!assetsSet.contains(line)) error("missing assets.")
|
|
lastEmoji = UnicodeEmoji(assetsName = line)
|
|
}
|
|
|
|
"drawable" -> {
|
|
val drawableId = getDrawableId(line) ?: error("missing drawable.")
|
|
lastEmoji = UnicodeEmoji(drawableId = drawableId)
|
|
}
|
|
|
|
"un" -> {
|
|
val emoji = lastEmoji ?: error("missing lastEmoji.")
|
|
addCode(emoji, line)
|
|
emoji.unifiedCode = line
|
|
}
|
|
|
|
"u" -> {
|
|
val emoji = lastEmoji ?: error("missing lastEmoji.")
|
|
addCode(emoji, line)
|
|
}
|
|
|
|
"sn" -> {
|
|
val emoji = lastEmoji ?: error("missing lastEmoji.")
|
|
addName(emoji, line)
|
|
emoji.unifiedName = line
|
|
}
|
|
|
|
"s" -> {
|
|
val emoji = lastEmoji ?: error("missing lastEmoji.")
|
|
addName(emoji, line)
|
|
}
|
|
|
|
"t" -> {
|
|
val cols = line.split(",", limit = 3)
|
|
if (cols.size != 3) error("invalid tone spec. line=$lno $line")
|
|
val parent = dst.unicodeMap[cols[0]]
|
|
?: error("missing tone parent. line=$lno $line")
|
|
val toneCode = cols[1].takeIf { it.isNotEmpty() }
|
|
?: error("missing tone code. line=$lno $line")
|
|
val child = dst.unicodeMap[cols[2]]
|
|
?: error("missing tone child. line=$lno $line")
|
|
parent.toneChildren.add(Pair(toneCode, child))
|
|
child.toneParent = parent
|
|
}
|
|
|
|
"cn" -> {
|
|
lastCategory = categoryNameMap[line]
|
|
?: error("missing category name.")
|
|
}
|
|
|
|
"c" -> {
|
|
val category = lastCategory
|
|
?: error("missing lastCategory.")
|
|
val emoji = dst.unicodeMap[line] ?: error("missing emoji.")
|
|
// if (emoji == null) {
|
|
// Log.w("SubwayTooter", "missing emoji. lno=$lno line=$rawLine")
|
|
// } else
|
|
if (!category.emojiList.contains(emoji)) {
|
|
category.emojiList.add(emoji)
|
|
}
|
|
}
|
|
|
|
else -> error("unknown header $head")
|
|
}
|
|
} catch (ex: Throwable) {
|
|
log.e(
|
|
ex,
|
|
"readEmojiDataLine: ${ex.javaClass.simpleName} ${ex.message} lno=$lno line=$rawLine"
|
|
)
|
|
// 行番号の情報をつけて投げ直す
|
|
errorEx(
|
|
ex,
|
|
"readEmojiDataLine: ${ex.javaClass.simpleName} ${ex.message} lno=$lno line=$rawLine"
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun ByteArray.indexOf(key: Byte, start: Int = 0): Int? {
|
|
var i = start
|
|
val end = this.size
|
|
while (i < end) {
|
|
if (this[i] == key) return i
|
|
++i
|
|
}
|
|
return null
|
|
}
|
|
|
|
private fun InputStream.eachLine(block: (Int, String) -> Unit) {
|
|
val lineFeed = 0x0a.toByte()
|
|
val buffer = ByteArray(4096)
|
|
// バッファに読む
|
|
var end = read(buffer, 0, buffer.size)
|
|
if (end <= 0) throw EOFException("unexpected EOF")
|
|
|
|
var lno = 0
|
|
while (true) {
|
|
var lineStart = 0
|
|
while (lineStart < end) {
|
|
// 行末記号を見つける
|
|
val feedPos = buffer.indexOf(lineFeed, lineStart) ?: break
|
|
++lno
|
|
if (feedPos > lineStart) {
|
|
// 1行分をUTF-8デコードして処理する
|
|
val line = String(buffer, lineStart, feedPos - lineStart, Charsets.UTF_8)
|
|
block(lno, line)
|
|
}
|
|
lineStart = feedPos + 1
|
|
}
|
|
// 最後の行末より後のデータをバッファ先頭に移動する
|
|
buffer.copyInto(buffer, 0, lineStart, end)
|
|
end -= lineStart
|
|
// ストリームから継ぎ足す
|
|
val nRead = read(buffer, end, buffer.size - end)
|
|
if (nRead <= 0) {
|
|
if (end > 0) throw EOFException("unexpected EOF")
|
|
break
|
|
}
|
|
end += nRead
|
|
}
|
|
}
|
|
|
|
fun readStream(inStream: InputStream) {
|
|
inStream.eachLine { lno, line -> readEmojiDataLine(lno, line) }
|
|
}
|
|
}
|