tags.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  2. <script src="https://unpkg.com/papaparse@5.4.1/papaparse.min.js"></script>
  3. <link rel="preconnect" href="https://fonts.googleapis.com">
  4. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  5. <body>
  6. <div id="app">
  7. <link v-for="family in uniqueFamilies" :href="familyLink(family)" rel="stylesheet">
  8. <h1>Google Fonts Tagger</h1>
  9. <div id="panel">
  10. <div class="panel-tile">
  11. <label>Current tag:</label>
  12. <select v-model="CurrentCategory" style="max-width: 300px;">
  13. <option v-for="category in sortedCategories" :key="category" :value="category">
  14. {{ category }}
  15. </option>
  16. </select>
  17. </div>
  18. <div class="panel-tile">
  19. <form @submit.prevent="AddFamily">
  20. <label>Add family:</label>
  21. <input list="items" v-model="newFamily" required placeholder="Family Name">
  22. <datalist id="items">
  23. <option v-for="family in uniqueFamilies" :value="family">
  24. </datalist>
  25. <input v-model="newWeight" required placeholder="Score">
  26. <button>Add</button>
  27. </form>
  28. </div>
  29. <div style="border: 0.1px solid rgb(161, 161, 161); margin-bottom: 10pt; margin-top: 10pt;"></div>
  30. <div class="panel-tile" style="max-height: 100px; overflow: scroll;">
  31. <label>History</label>
  32. <div v-if="isEdited">
  33. <p style="font-size: 10pt;" v-for="item in history">{{ item }}</p>
  34. </div>
  35. <div v-else>
  36. <p style="font-size: 10pt;">No changes</p>
  37. </div>
  38. </div>
  39. <div style="border: 0.1px solid rgb(161, 161, 161); margin-bottom: 10pt; margin-top: 10pt;"></div>
  40. <div class="panel-tile">
  41. <button @click="prCSV">Open Pull Request</button>
  42. <button style="float: right;" @click="saveCSV">Save CSV</button>
  43. </div>
  44. </div>
  45. <div class="item" v-for="family in sortedFamilies" :key="family.Family">
  46. <div style="float: left; width: 150px;">
  47. <b>{{ family.Family }}</b>
  48. </div>
  49. <div style="float: left; width: 100px;">
  50. <input style="width: 50px;" v-model.lazy="family.Weight" @change="edited(family)" placeholder="family.Weight">
  51. <button @click="removeFamily(family)">X</button>
  52. </div>
  53. <div v-if="ready" :style="familyStyle(family)" contenteditable="true">
  54. {{ familyPangram(family) }}
  55. </div>
  56. <div v-else>
  57. Loading...
  58. </div>
  59. </div>
  60. </div>
  61. </body>
  62. <script>
  63. var app = new Vue({
  64. el: '#app',
  65. data() {
  66. return {
  67. ready: false,
  68. isEdited: false,
  69. newFamily: '',
  70. newWeight: '',
  71. CurrentCategory: "/Expressive/Calm",
  72. Categories: new Set(),
  73. Families: [],
  74. Pangrams: new Map([
  75. ["English", "The quick brown fox jumps over the lazy dog."],
  76. ["Greek", "Ζαφείρι δέξου πάγκαλο, βαθῶν ψυχῆς τὸ σῆμα"],
  77. ["Cyrillic", "В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!"],
  78. ["Japanese", "いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす(ん"],
  79. ["Chinese", "視野無限廣,窗外有藍天"],
  80. ["Arabic", "نص حكيم له سر قاطع وذو شأن عظيم مكتوب على ثوب أخضر ومغلف بجلد أزرق"],
  81. ["Hebrew", "שפן אכל קצת גזר בטעם חסה, ודי."],
  82. ["Devanagari", "ऋषियों को सताने वाले दुष्ट राक्षसों के राजा रावण का सर्वनाश करने वाले विष्णुवतार भगवान श्रीराम, अयोध्या के महाराज दशरथ के बड़े सपुत्र थे।"],
  83. ["Bengali", "যেহেতু মানব পরিবারের সকল সদস্যের সমান ও অবিচ্ছেদ্য অধিকারসমূহ"],
  84. ["Gujarati", "કેમ કે માનવકુટુંબના દરેક સભ્યની પરંપરાપ્રાપ્ત પ્રતિષ્ઠાને અને"],
  85. ["Telugu", "మానవకుటంబమునందలి వ్యక్తులందరికిని గల ఆజన్మసిద్ధమైన ప్రతిపత్తిని"],
  86. ["Kannada", "ಎಲ್ಲಾ ಮಾನವರೂ ಸ್ವತಂತ್ರರಾಗಿಯೇ ಜನಿಸಿದ್ದಾರೆ. ಹಾಗೂ ಘನತೆ ಮತ್ತು ಹಕ್ಕುಗಳಲ್ಲಿ"],
  87. ["Khmer", "ដោយយល់ឃើញថា ការទទួលស្គាល់សេចក្ដីថ្លៃថ្នូរជាប់ពីកំណើត និងសិទ្ធិស្មើភាពគ្នា"],
  88. ["Phags Pa", "ꡗ ꡈꡱ ᠂ ꡒ ꡂ ꡈꡞ ᠂ ꡚꡖꡋ ꡈꡞꡋꡨꡖ ꡗꡛꡧꡖ ꡈꡋ ꡈꡱꡨꡖ ꡳꡬꡖ"],
  89. ["Tamil", "மனிதக் குடும்பத்தினைச் சேர்ந்த யாவரதும் உள்ளார்ந்த"],
  90. ]),
  91. FamilyScripts: new Map(),
  92. history: [],
  93. };
  94. },
  95. created() {
  96. this.loadCSV();
  97. this.loadFamilyPangrams();
  98. },
  99. computed: {
  100. sortedFamilies() {
  101. let ll = this.Families;
  102. let filtered = ll.filter(family => family["Group/Tag"] === this.CurrentCategory);
  103. filtered.sort(function(a, b) {return b.Weight - a.Weight;});
  104. return filtered;
  105. },
  106. uniqueFamilies() {
  107. return Array.from(new Set(this.Families.map((family) => family.Family)));
  108. },
  109. sortedCategories() {
  110. return Array.from(this.Categories).sort();
  111. }
  112. },
  113. methods: {
  114. familyPangram(family) {
  115. return this.Pangrams.get(this.FamilyScripts.get(family.Family));
  116. },
  117. edited(family) {
  118. this.isEdited = true;
  119. this.history.push(`* ${family.Family},${family["Group/Tag"]},${family.Weight}`);
  120. },
  121. parseUnicode(str) {
  122. let ranges = str.split(",");
  123. let script = "English";
  124. let scripts = {
  125. "U+600-6FF": "Arabic",
  126. "U+900-97F": "Devanagari",
  127. "U+590-5FF": "Hebrew",
  128. "U+A80-AFF": "Gujarati",
  129. "U+C00-C7F": "Telugu",
  130. "U+C80-CFF": "Kannada",
  131. "U+980-9FE": "Bengali",
  132. "U+1780-17FF": "Khmer",
  133. "U+A840-A877": "Phags Pa",
  134. "U+0B82-0BFA": "Tamil",
  135. }
  136. for (let i = 0; i < ranges.length; i++) {
  137. for (let key in scripts) {
  138. if (ranges[i].includes(key)) {
  139. script = scripts[key];
  140. break;
  141. }
  142. }
  143. }
  144. return script;
  145. },
  146. async loadFamilyPangrams(delay = 1000) {
  147. await document.fonts.ready;
  148. let result = new Map();
  149. let fonts = document.fonts;
  150. fonts.forEach((font) => {
  151. if (!result.has(font.family)) {
  152. result.set(font.family, this.parseUnicode(font.unicodeRange));
  153. }
  154. });
  155. console.log(result.size)
  156. if (result.size < 1000) {
  157. console.log("retry")
  158. setTimeout(() => this.loadFamilyPangrams(), delay);
  159. }
  160. this.FamilyScripts = result;
  161. this.ready = true;
  162. },
  163. familyLink(Family) {
  164. return "https://fonts.googleapis.com/css2?family=" + Family.replace(" ", "+") + "&display=swap"
  165. },
  166. familyCSSClass(Family) {
  167. let cssName = Family.family.replace(" ", "-").toLowerCase();
  168. return `.${cssName} {
  169. font-family: "${Family.family}", sans-serif;
  170. font-weight: 400;
  171. font-style: normal;
  172. }`
  173. },
  174. familySelector(Family) {
  175. let cssName = Family.Family.replace(" ", "-").toLowerCase();
  176. return cssName;
  177. },
  178. familyStyle(Family) {
  179. return `font-family: "${Family.Family}", "Adobe NotDef"; font-size: 32pt;`
  180. },
  181. AddFamily() {
  182. this.isEdited = true;
  183. let newFamily = { Weight: this.newWeight, Family: this.newFamily, "Group/Tag": this.CurrentCategory }
  184. this.Families.push(newFamily);
  185. this.history.push(`+ ${newFamily.Family},${newFamily["Group/Tag"]},${newFamily.Weight}`);
  186. },
  187. removeFamily(Family) {
  188. this.isEdited = true;
  189. this.Families = this.Families.filter((t) => t !== Family);
  190. this.history.push(`- ${Family.Family},${Family["Group/Tag"]},${Family.Weight}`);
  191. },
  192. familiesToCSV() {
  193. this.Families = this.Families.filter((t) => t.Family !== "");
  194. // The sorting function used is case sensitive.
  195. // This means that "A" will come before "a".
  196. this.Families = Array.from(this.Families).sort((a, b) => {
  197. if (`${a.Family},${a['Group/Tag']}` < `${b.Family},${b['Group/Tag']}`) {
  198. return -1;
  199. }
  200. if (`${a.Family},${a['Group/Tag']}` > `${b.Family},${b['Group/Tag']}`) {
  201. return 1;
  202. }
  203. return 0;
  204. });
  205. // Include a newline at the end to keep Evan's Vim happy.
  206. return Papa.unparse(this.Families,
  207. {
  208. columns: ["Family", "Group/Tag", "Weight"],
  209. skipEmptyLines: true,
  210. }
  211. ) + "\n";
  212. },
  213. saveCSV() {
  214. let csv = this.familiesToCSV();
  215. const blob = new Blob([csv], { type: 'text/csv' });
  216. const url = URL.createObjectURL(blob);
  217. const a = document.createElement('a');
  218. a.href = url;
  219. a.download = "families.csv";
  220. document.body.appendChild(a);
  221. a.click();
  222. document.body.removeChild(a);
  223. URL.revokeObjectURL(url);
  224. },
  225. prCSV() {
  226. let csv = this.familiesToCSV();
  227. alert("Tag data copied to clipboard. A github pull request page will open in a new tab. Please remove the old data and paste in the new.");
  228. navigator.clipboard.writeText(csv);
  229. window.open("https://github.com/google/fonts/edit/main/tags/all/families.csv")
  230. },
  231. loadCSV() {
  232. const csvFilePath = 'https://raw.githubusercontent.com/google/fonts/main/tags/all/families.csv'; // Update this path to your CSV file
  233. fetch(csvFilePath)
  234. .then(response => response.text())
  235. .then(csvText => {
  236. Papa.parse(csvText, {
  237. header: true,
  238. complete: (results) => {
  239. this.Categories = new Set(results.data.map((row) => row["Group/Tag"]));
  240. this.Families = results.data.map((row) => ({
  241. Weight: row.Weight,
  242. Family: row.Family,
  243. "Group/Tag": row["Group/Tag"]
  244. })
  245. );
  246. }
  247. });
  248. })
  249. .catch(error => {
  250. console.error('Error loading CSV file:', error);
  251. });
  252. }
  253. }
  254. } // methods
  255. )
  256. </script>
  257. <style>
  258. @font-face {
  259. font-family: "Adobe NotDef";
  260. src: url(https://cdn.jsdelivr.net/gh/adobe-fonts/adobe-notdef/AND-Regular.ttf);
  261. }
  262. #app {
  263. font-family: "Roboto", Helvetica, Arial, sans-serif;
  264. -webkit-font-smoothing: antialiased;
  265. -moz-osx-font-smoothing: grayscale;
  266. text-align: left;
  267. color: #2c3e50;
  268. margin-top: 60px;
  269. }
  270. #panel {
  271. position: fixed;
  272. top: 0;
  273. right: 0;
  274. padding: 10px;
  275. background-color: white;
  276. box-shadow: 3px 3px 3px lightgray;
  277. }
  278. .panel-tile {
  279. margin-bottom: 10px;
  280. }
  281. .familyy {
  282. padding-top: 10px;
  283. padding-bottom: 10px;
  284. }
  285. .item {
  286. margin-top: 10px;
  287. padding-top: 10px;
  288. padding-bottom: 10px;
  289. border-top: 1px solid #000;
  290. }
  291. #edited-panel {
  292. position: fixed;
  293. left: 0px;
  294. top: 0px;
  295. width: 80px;
  296. padding: 5px;
  297. background-color: black;
  298. color: white;
  299. font-weight: bold;
  300. }
  301. </style>