#!/usr/bin/python # -*- coding: utf-8 -*- # # This script generates a data file containing all Unicode information needed by KCharSelect. # ############################################################################# # SPDX-FileCopyrightText: 2007 Daniel Laidig # # SPDX-License-Identifier: LGPL-2.1-or-later ############################################################################# # # The current directory must contain the following files that can be found at # http://www.unicode.org/Public/UNIDATA/: # - UnicodeData.txt # - Unihan_Readings.txt (you need to uncompress it from Unihan.zip) # - NamesList.txt # - Blocks.txt # # The generated file is named "kcharselect-data" and has to be put in kdelibs/kdeui/widgets/. # Additionally a translation dummy named "kcharselect-translation.cpp" is generated and has # to be placed in the same directory. # # FILE STRUCTURE # # The generated file is a binary file. The first 40 bytes are the header # and contain the position of each part of the file. Each entry is uint32. # # pos content # 0 names strings begin # 4 names offsets begin # 8 details strings begin # 12 details offsets begin # 16 block strings begin # 20 block offsets begin # 24 section strings begin # 28 section offsets begin # 32 unihan strings begin # 36 unihan offsets begin # # The string parts always contain all strings in a row, followed by a 0x00 byte. # There is one exception: The data for seeAlso in details is only 2 bytes (as is always is _one_ # unicode character) and _not_ followed by a 0x00 byte. # # The offset parts contain entries with a fixed length. Unicode characters are always uint16 and offsets uint32. # Offsets are positions in the data file. # # names_offsets: # each entry 6 bytes # 16bit: unicode # 32bit: offset to name in names_strings # # names_strings: # the first byte is the category (same values as QChar::Category), # directly followed by the character name (terminated by 0x00) # # nameslist_offsets: # char, alias, alias_count, note, note_count, approxEquiv, approxEquiv_coutn, equiv, equiv_count, seeAlso, seeAlso_count # 16 32 8 32 8 32 8 32 8 32 8 # => each entry 27 bytes # # blocks_offsets: # each entry 4 bytes # 16bit: start unicode # 16bit: end unicode # Note that there is no string offset. # # section_offsets: # each entry 4 bytes # 16bit: section offset # 16bit: block offset # Note that these offsets are _not_ positions in the data file but indexes. # For example 0x0403 means the fourth section includes the third block. # # unihan_offsets: # each entry 30 bytes # 16bit: unicode # 32bit: offset to unihan_strings for Definition # 32bit: offset to unihan_strings for Cantonese # 32bit: offset to unihan_strings for Mandarin # 32bit: offset to unihan_strings for Tang # 32bit: offset to unihan_strings for Korean # 32bit: offset to unihan_strings for JapaneseKun # 32bit: offset to unihan_strings for JapaneseOn from struct import * import sys import re import io # based on http://www.unicode.org/charts/ sectiondata = ''' SECTION European Scripts Armenian Carian Caucasian Albanian Cypriot Syllabary Cypro-Minoan Cyrillic Cyrillic Supplement Cyrillic Extended-A Cyrillic Extended-B Cyrillic Extended-C Cyrillic Extended-D Elbasan Georgian Georgian Extended Georgian Supplement Glagolitic Glagolitic Supplement Gothic Greek and Coptic Greek Extended Ancient Greek Numbers Basic Latin Latin-1 Supplement Latin Extended-A Latin Extended-B Latin Extended-C Latin Extended-D Latin Extended-E Latin Extended-F Latin Extended-G Latin Extended Additional IPA Extensions Phonetic Extensions Phonetic Extensions Supplement Linear A Linear B Syllabary Linear B Ideograms Aegean Numbers Lycian Lydian Ogham Old Hungarian Old Italic Old Permic Phaistos Disc Runic Shavian Vithkuqi SECTION Modifier Letters Modifier Tone Letters Spacing Modifier Letters Superscripts and Subscripts SECTION Combining Marks Combining Diacritical Marks Combining Diacritical Marks Extended Combining Diacritical Marks Supplement Combining Diacritical Marks for Symbols Combining Half Marks SECTION African Scripts Adlam Bamum Bamum Supplement Bassa Vah Coptic Coptic Epact Numbers Egyptian Hieroglyphs Egyptian Hieroglyph Format Controls Ethiopic Ethiopic Supplement Ethiopic Extended Ethiopic Extended-A Ethiopic Extended-B Medefaidrin Mende Kikakui Meroitic Cursive Meroitic Hieroglyphs NKo Osmanya Tifinagh Vai SECTION Middle Eastern Scripts Anatolian Hieroglyphs Arabic Arabic Supplement Arabic Extended-A Arabic Extended-B Arabic Extended-C Arabic Presentation Forms-A Arabic Presentation Forms-B Imperial Aramaic Avestan Chorasmian Carian Cuneiform Cuneiform Numbers and Punctuation Early Dynastic Cuneiform Old Persian Ugaritic Elymaic Hatran Hebrew Mandaic Nabataean Old North Arabian Old South Arabian Inscriptional Pahlavi Psalter Pahlavi Palmyrene Inscriptional Parthian Phoenician Samaritan Syriac Syriac Supplement Yezidi SECTION Central Asian Scripts Manichaean Marchen Mongolian Mongolian Supplement Old Sogdian Old Turkic Old Uyghur Phags-pa Sogdian Soyombo Tibetan Zanabazar Square SECTION South Asian Scripts Ahom Bengali Bhaiksuki Brahmi Chakma Devanagari Devanagari Extended Devanagari Extended-A Dives Akuru Dogra Grantha Gujarati Gunjala Gondi Gurmukhi Kaithi Kannada Kharoshthi Khojki Khudawadi Lepcha Limbu Mahajani Malayalam Masaram Gondi Meetei Mayek Meetei Mayek Extensions Modi Mro Multani Nag Mundari Nandinagari Newa Ol Chiki Oriya Saurashtra Sharada Siddham Sinhala Sinhala Archaic Numbers Sora Sompeng Syloti Nagri Takri Tamil Tamil Supplement Telugu Thaana Tirhuta Toto Vedic Extensions Wancho Warang Citi SECTION Southeast Asian Scripts Cham Hanifi Rohingya Kayah Li Khmer Khmer Symbols Lao Myanmar Myanmar Extended-A Myanmar Extended-B New Tai Lue Nyiakeng Puachue Hmong Pahawh Hmong Pau Cin Hau Tai Le Tai Tham Tai Viet Tangsa Thai SECTION Indonesia & Oceania Scripts Balinese Batak Buginese Buhid Hanunoo Javanese Kawi Makasar Rejang Sundanese Sundanese Supplement Tagalog Tagbanwa SECTION East Asian Scripts Bopomofo Bopomofo Extended CJK Unified Ideographs CJK Unified Ideographs Extension A CJK Unified Ideographs Extension B CJK Unified Ideographs Extension C CJK Unified Ideographs Extension D CJK Unified Ideographs Extension E CJK Unified Ideographs Extension F CJK Unified Ideographs Extension G CJK Unified Ideographs Extension H CJK Compatibility Ideographs CJK Compatibility Ideographs Supplement Kangxi Radicals CJK Radicals Supplement CJK Strokes Ideographic Description Characters Hangul Jamo Hangul Jamo Extended-A Hangul Jamo Extended-B Hangul Compatibility Jamo Hangul Syllables Hiragana Kana Extended-A Kana Extended-B Kana Supplement Small Kana Extension Kanbun Katakana Katakana Phonetic Extensions Khitan Small Script Lisu Lisu Supplement Miao Nushu Tangut Tangut Components Tangut Supplement Yi Syllables Yi Radicals SECTION American Scripts Cherokee Cherokee Supplement Deseret Osage Unified Canadian Aboriginal Syllabics Unified Canadian Aboriginal Syllabics Extended Unified Canadian Aboriginal Syllabics Extended-A SECTION Other Alphabetic Presentation Forms Halfwidth and Fullwidth Forms SECTION Notational Systems Braille Patterns Musical Symbols Ancient Greek Musical Notation Byzantine Musical Symbols Znamenny Musical Notation Duployan Shorthand Format Controls Sutton SignWriting SECTION Punctuation General Punctuation Supplemental Punctuation CJK Symbols and Punctuation Ideographic Symbols and Punctuation CJK Compatibility Forms Halfwidth and Fullwidth Forms Small Form Variants Vertical Forms SECTION Alphanumeric Symbols Letterlike Symbols Mathematical Alphanumeric Symbols Arabic Mathematical Alphabetic Symbols Enclosed Alphanumerics Enclosed Alphanumeric Supplement Enclosed CJK Letters and Months Enclosed Ideographic Supplement CJK Compatibility SECTION Technical Symbols Control Pictures Miscellaneous Technical Optical Character Recognition SECTION Numbers & Digits Common Indic Number Forms Coptic Epact Numbers Counting Rod Numerals Cuneiform Numbers and Punctuation Indic Siyaq Numbers Kaktovik Numerals Mayan Numerals Number Forms Ottoman Siyaq Numbers Rumi Numeral Symbols Sinhala Archaic Numbers SECTION Mathematical Symbols Arrows Supplemental Arrows-A Supplemental Arrows-B Supplemental Arrows-C Miscellaneous Symbols and Arrows Mathematical Alphanumeric Symbols Arabic Mathematical Alphabetic Symbols Letterlike Symbols Mathematical Operators Supplemental Mathematical Operators Miscellaneous Mathematical Symbols-A Miscellaneous Mathematical Symbols-B Geometric Shapes Box Drawing Block Elements Geometric Shapes Extended SECTION Emoji & Pictographs Dingbats Ornamental Dingbats Emoticons Miscellaneous Symbols Miscellaneous Symbols and Pictographs Supplemental Symbols and Pictographs Symbols and Pictographs Extended-A Transport and Map Symbols SECTION Other Symbols Alchemical Symbols Ancient Symbols Currency Symbols Chess Symbols Domino Tiles Mahjong Tiles Playing Cards Miscellaneous Symbols and Arrows Symbols for Legacy Computing Yijing Hexagram Symbols Tai Xuan Jing Symbols SECTION Specials Specials Tags Variation Selectors Variation Selectors Supplement SECTION Private Use Private Use Area Supplementary Private Use Area-A Supplementary Private Use Area-B SECTION Surrogates High Surrogates High Private Use Surrogates Low Surrogates ''' # TODO: rename "Other Scripts" to "American Scripts" categoryMap = { # same values as QChar::Category "Mn": 1, "Mc": 2, "Me": 3, "Nd": 4, "Nl": 5, "No": 6, "Zs": 7, "Zl": 8, "Zp": 9, "Cc": 10, "Cf": 11, "Cs": 12, "Co": 13, "Cn": 14, "Lu": 15, "Ll": 16, "Lt": 17, "Lm": 18, "Lo": 19, "Pc": 20, "Pd": 21, "Ps": 22, "Pe": 23, "Pi": 24, "Pf": 25, "Po": 26, "Sm": 27, "Sc": 28, "Sk": 29, "So": 30 } class Names: def __init__(self): self.names = [] self.controlpos = -1 def addName(self, uni, name, category): self.names.append([uni, name, category]) def calculateStringSize(self): size = 0 hadcontrol = False for entry in self.names: if entry[1] == "": if not hadcontrol: size += len(entry[1].encode('utf-8')) + 2 hadcontrol = True else: size += len(entry[1].encode('utf-8')) + 2 return size def calculateOffsetSize(self): return len(self.names)*8 def writeStrings(self, out, pos): hadcontrol = False for entry in self.names: if entry[1] == "": if not hadcontrol: out.write(pack("=b", entry[2])) out.write(entry[1].encode('utf-8') + b"\0") size = len(entry[1].encode('utf-8')) + 2 entry[1] = pos self.controlpos = pos pos += size hadcontrol = True else: entry[1] = self.controlpos else: out.write(pack("=b", entry[2])) out.write(entry[1].encode('utf-8') + b"\0") size = len(entry[1].encode('utf-8')) + 2 entry[1] = pos pos += size return pos def writeOffsets(self, out, pos): for entry in self.names: out.write(pack("=II", int(entry[0], 16), entry[1])) pos += 8 return pos class Details: def __init__(self): self.details = {} def addEntry(self, char, category, text): if char not in self.details: self.details[char] = {} if category not in self.details[char]: self.details[char][category] = [] self.details[char][category].append(text) def calculateStringSize(self): size = 0 for char in self.details.values(): for cat in char.values(): for s in cat: if type(s) is str: size += len(s.encode('utf-8')) + 1 else: size += 4 return size def calculateOffsetSize(self): return len(self.details)*29 def writeStrings(self, out, pos): for char in self.details.values(): for cat in char.values(): for i in range(0, len(cat)): s = cat[i] if type(s) is str: out.write(s.encode('utf-8') + b"\0") size = len(s.encode('utf-8')) + 1 else: out.write(pack("=I", s)) size = 4 cat[i] = pos pos += size return pos def writeOffsets(self, out, pos): for char in self.details.keys(): alias = 0 alias_count = 0 note = 0 note_count = 0 approxEquiv = 0 approxEquiv_count = 0 equiv = 0 equiv_count = 0 seeAlso = 0 seeAlso_count = 0 if "alias" in self.details[char]: alias = self.details[char]["alias"][0] alias_count = len(self.details[char]["alias"]) if "note" in self.details[char]: note = self.details[char]["note"][0] note_count = len(self.details[char]["note"]) if "approxEquiv" in self.details[char]: approxEquiv = self.details[char]["approxEquiv"][0] approxEquiv_count = len(self.details[char]["approxEquiv"]) if "equiv" in self.details[char]: equiv = self.details[char]["equiv"][0] equiv_count = len(self.details[char]["equiv"]) if "seeAlso" in self.details[char]: seeAlso = self.details[char]["seeAlso"][0] seeAlso_count = len(self.details[char]["seeAlso"]) out.write(pack("=IIbIbIbIbIb", char, alias, alias_count, note, note_count, approxEquiv, approxEquiv_count, equiv, equiv_count, seeAlso, seeAlso_count)) pos += 29 return pos class SectionsBlocks: def __init__(self): self.sections = [] self.blocks = [] self.blockList = [] self.sectionList = [] def addBlock(self, begin, end, name): self.blocks.append([begin, end, name]) self.blockList.append(name) def addSection(self, section, block): self.sections.append([section, block]) if not section in self.sectionList: self.sectionList.append(section) def calculateBlockStringSize(self): size = 0 for block in self.blocks: size += len(block[2].encode('utf-8')) + 1 return size def calculateBlockOffsetSize(self): return len(self.blocks) * 8 def calculateSectionStringSize(self): size = 0 lastsection = "" for section in self.sections: if section[0] != lastsection: size += len(section[0].encode('utf-8')) + 1 lastsection = section[0] return size def calculateSectionOffsetSize(self): return len(self.sections) * 8 def writeBlockStrings(self, out, pos): index = 0 for block in self.blocks: out.write(block[2].encode('utf-8') + b"\0") size = len(block[2].encode('utf-8')) + 1 found = False for section in self.sections: print(section) if section[1] == block[2]: print("found", section) section[1] = int(index) found = True if not found: print("Error: Did not find any category for block \""+block[2]+"\"") sys.exit(1) block[2] = index pos += size index += 1 return pos def writeBlockOffsets(self, out, pos): for block in self.blocks: out.write(pack("=II", int(block[0], 16), int(block[1], 16))) pos += 8 return pos def writeSectionStrings(self, out, pos): lastsection = "" lastpos = 0 index = -1 for section in self.sections: if section[0] != lastsection: index += 1 lastsection = section[0] out.write(section[0].encode('utf-8') + b"\0") size = len(section[0].encode('utf-8')) + 1 section[0] = index lastpos = pos pos += size else: section[0] = index return pos def writeSectionOffsets(self, out, pos): print(self.sections) for section in self.sections: out.write(pack("=II", section[0], section[1])) pos += 8 return pos def getBlockList(self): return self.blockList def getSectionList(self): return self.sectionList class Unihan: def __init__(self): self.unihan = {} def addUnihan(self, uni, category, value): uni = int(uni, 16) if category != "kDefinition" and category != "kCantonese" and category != "kMandarin" and category != "kTang" and category != "kKorean" and category != "kJapaneseKun" and category != "kJapaneseOn": return if uni not in self.unihan: self.unihan[uni] = [None, None, None, None, None, None, None] if category == "kDefinition": self.unihan[uni][0] = value elif category == "kCantonese": self.unihan[uni][1] = value elif category == "kMandarin": self.unihan[uni][2] = value elif category == "kTang": self.unihan[uni][3] = value elif category == "kKorean": self.unihan[uni][4] = value elif category == "kJapaneseKun": self.unihan[uni][5] = value elif category == "kJapaneseOn": self.unihan[uni][6] = value def calculateStringSize(self): size = 0 for char in self.unihan.keys(): for entry in self.unihan[char]: if entry != None: size += len(entry.encode('utf-8')) + 1 return size def calculateOffsetSize(self): return len(self.unihan) * 32 def writeStrings(self, out, pos): for char in self.unihan.keys(): for i in range(0, 7): if self.unihan[char][i] != None: out.write(self.unihan[char][i].encode('utf-8') + b"\0") size = len(self.unihan[char][i].encode('utf-8')) + 1 self.unihan[char][i] = pos pos += size return pos def writeOffsets(self, out, pos): for char in self.unihan.keys(): out.write(pack("=I", char)) for i in range(0, 7): if self.unihan[char][i] != None: out.write(pack("=I", self.unihan[char][i])) else: out.write(pack("=I", 0)) pos += 32 return pos class Parser: def parseUnicodeData(self, inUnicodeData, names): regexp = re.compile(r'^([^;]+);([^;]+);([^;]+)') for line in inUnicodeData: line = line[:-1] m = regexp.match(line) if not m: continue uni = m.group(1) name = m.group(2) category = m.group(3) if len(uni) > 8: continue names.addName(uni, name, categoryMap[category]) def parseDetails(self, inNamesList, details): invalidRegexp = re.compile(r'^@') unicodeRegexp = re.compile(r'^([0-9A-F]+)') aliasRegexp = re.compile(r'^\s+=\s+(.+)$') #equal seeAlsoRegexp = re.compile(r'^\s+x\s+.*([0-9A-F]{4,6})\)$') #ex noteRegexp = re.compile(r'^\s+\*\s+(.+)$') #star approxEquivalentRegexp = re.compile(r'^\s+#\s+(.+)$') #pound equivalentRegexp = re.compile(r'^\s+:\s+(.+)$') #colon drop = 0 currChar = 0 for line in inNamesList: line = line[:-1] m1 = unicodeRegexp.match(line) m2 = aliasRegexp.match(line) m3 = noteRegexp.match(line) m4 = approxEquivalentRegexp.match(line) m5 = equivalentRegexp.match(line) m6 = seeAlsoRegexp.match(line) if invalidRegexp.match(line): continue elif m1: currChar = int(m1.group(1), 16) if len(m1.group(1)) > 8: #limit to 32bit drop = 1 continue elif drop == 1: continue elif m2: value = m2.group(1) details.addEntry(currChar, "alias", value) elif m3: value = m3.group(1) details.addEntry(currChar, "note", value) elif m4: value = m4.group(1) details.addEntry(currChar, "approxEquiv", value) elif m5: value = m5.group(1) details.addEntry(currChar, "equiv", value) elif m6: value = int(m6.group(1), 16) details.addEntry(currChar, "seeAlso", value) def parseBlocks(self, inBlocks, sectionsBlocks): regexp = re.compile(r'^([0-9A-F]+)\.\.([0-9A-F]+); (.+)$') for line in inBlocks: line = line[:-1] m = regexp.match(line) if not m: continue if len(m.group(1)) > 8: continue sectionsBlocks.addBlock(m.group(1), m.group(2), m.group(3)) def parseSections(self, inSections, sectionsBlocks): currSection = "" for line in inSections: line = line[:-1] if len(line) == 0: continue temp = line.split(" ") if temp[0] == "SECTION": currSection = line[8:] elif currSection != "" and line != "": sectionsBlocks.addSection(currSection, line) else: print("error in data file") sys.exit(1) def parseUnihan(self, inUnihan, unihan): regexp = re.compile(r'^U\+([0-9A-F]+)\s+([^\s]+)\s+(.+)$') count = 0 for line in inUnihan: if count % 100000 == 0: print("\b.", end=' ') sys.stdout.flush() count += 1 line = line[:-1] m = regexp.match(line) if not m: continue if len(m.group(1)) <= 4: unihan.addUnihan(m.group(1), m.group(2), m.group(3)) def writeTranslationDummy(out, data): out.write(b"""\n\n""") for group in data: for entry in group[1]: out.write(b"I18N_NOOP2(\""+group[0].encode('utf-8')+b"\", \""+entry.encode('utf-8')+b"\");\n") out = open("kcharselect-data", "wb") outTranslationDummy = open("kcharselect-translation.cpp", "wb") inUnicodeData = open("UnicodeData.txt", "r") inNamesList = open("NamesList.txt", "r") inBlocks = open("Blocks.txt", "r") inSections = io.StringIO(sectiondata) inUnihan = open("Unihan_Readings.txt", "r") if calcsize('=H') != 2 or calcsize('=I') != 4: print("Error: Sizes of ushort and uint are not 16 and 32 bit as expected") sys.exit(1) names = Names() details = Details() sectionsBlocks = SectionsBlocks() unihan = Unihan() parser = Parser() print("========== parsing files ===================") parser.parseUnicodeData(inUnicodeData, names) print(".", end=' ') sys.stdout.flush() parser.parseDetails(inNamesList, details) print("\b.", end=' ') sys.stdout.flush() parser.parseBlocks(inBlocks, sectionsBlocks) print("\b.", end=' ') sys.stdout.flush() parser.parseSections(inSections, sectionsBlocks) print("\b.", end=' ') sys.stdout.flush() parser.parseUnihan(inUnihan, unihan) print("\b.", end=' ') sys.stdout.flush() print("done.") pos = 0 #write header, size: 40 bytes print("========== writing header ==================") out.write(pack("=I", 40)) print("names strings begin", 40) namesOffsetBegin = names.calculateStringSize() + 40 out.write(pack("=I", namesOffsetBegin)) print("names offsets begin", namesOffsetBegin) detailsStringBegin = namesOffsetBegin + names.calculateOffsetSize() out.write(pack("=I", detailsStringBegin)) print("details strings begin", detailsStringBegin) detailsOffsetBegin = detailsStringBegin + details.calculateStringSize() out.write(pack("=I", detailsOffsetBegin)) print("details offsets begin", detailsOffsetBegin) blocksStringBegin = detailsOffsetBegin + details.calculateOffsetSize() out.write(pack("=I", blocksStringBegin)) print("block strings begin", blocksStringBegin) blocksOffsetBegin = blocksStringBegin + sectionsBlocks.calculateBlockStringSize() out.write(pack("=I", blocksOffsetBegin)) print("block offsets begin", blocksOffsetBegin) sectionStringBegin = blocksOffsetBegin + sectionsBlocks.calculateBlockOffsetSize() out.write(pack("=I", sectionStringBegin)) print("section strings begin", sectionStringBegin) sectionOffsetBegin = sectionStringBegin + sectionsBlocks.calculateSectionStringSize() out.write(pack("=I", sectionOffsetBegin)) print("section offsets begin", sectionOffsetBegin) unihanStringBegin = sectionOffsetBegin + sectionsBlocks.calculateSectionOffsetSize() out.write(pack("=I", unihanStringBegin)) print("unihan strings begin", unihanStringBegin) unihanOffsetBegin = unihanStringBegin + unihan.calculateStringSize() out.write(pack("=I", unihanOffsetBegin)) print("unihan offsets begin", unihanOffsetBegin) end = unihanOffsetBegin + unihan.calculateOffsetSize() print("end should be", end) pos += 40 print("========== writing data ====================") pos = names.writeStrings(out, pos) print("names strings written, position", pos) pos = names.writeOffsets(out, pos) print("names offsets written, position", pos) pos = details.writeStrings(out, pos) print("details strings written, position", pos) pos = details.writeOffsets(out, pos) print("details offsets written, position", pos) pos = sectionsBlocks.writeBlockStrings(out, pos) print(sectionsBlocks.sections) print("block strings written, position", pos) pos = sectionsBlocks.writeBlockOffsets(out, pos) print("block offsets written, position", pos) pos = sectionsBlocks.writeSectionStrings(out, pos) print("section strings written, position", pos) pos = sectionsBlocks.writeSectionOffsets(out, pos) print("section offsets written, position", pos) pos = unihan.writeStrings(out, pos) print("unihan strings written, position", pos) pos = unihan.writeOffsets(out, pos) print("unihan offsets written, position", pos) print("========== writing translation dummy ======") translationData = [["KCharSelect section name", sectionsBlocks.getSectionList()], ["KCharselect unicode block name",sectionsBlocks.getBlockList()]] writeTranslationDummy(outTranslationDummy, translationData) print("done. make sure to copy both kcharselect-data and kcharselect-translation.cpp.")