tags.html 14 KB

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