123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
- <script src="https://unpkg.com/papaparse@5.4.1/papaparse.min.js"></script>
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
- <link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.14/dist/full.min.css" rel="stylesheet" type="text/css" />
- <script src="https://cdn.tailwindcss.com"></script>
- <body>
- <div id="app">
- <link v-for="family in uniqueFamilies" :href="familyLink(family)" rel="stylesheet">
- <!-- Navbar -->
- <div style="max-height: 3rem" class="navbar bg-base-100 fixed left-0 top-0 shadow">
- <a class="btn btn-ghost btn-sm text-xl">GF Tagger</a>
- <ul class="menu menu-horizontal px-1">
- <li>
- <details>
- <summary>File</summary>
- <ul class="shadow w-36">
- <li><a @click="saveCSV">Export CSV</a></li>
- <li><a @click="prCSV">Open PR</a></li>
- </ul>
- </details>
- </li>
- <li>
- <details>
- <summary>Edit</summary>
- <ul class="shadow w-56">
- <li><a @click="AddPlaceHolderTags">Insert placeholder tags</a></li>
- <li><a @click="RemovePlaceHolderTags">Remove placeholder tags</a></li>
- </ul>
- </details>
- </li>
- <li>
- <details>
- <summary>{{ CurrentCategory }}</summary>
- <ul class="shadow">
- <div class="cont" style="max-height: 16rem; overflow: scroll">
- <li v-for="category in sortedCategories()">
- <a @click="CurrentCategory = category">{{ category }}</a>
- </li>
- </div>
- </ul>
- </details>
- </li>
- <li>
- <form @submit.prevent="loadCSV">
- <div class="join" style="padding-top: 12px;">
- <input class="join-item input input-sm input-bordered w-full max-w-xs" v-model="commit" required placeholder="refs/heads/main">
- <button class="join-item btn btn-sm">checkout</button>
- </div>
- </form>
- </li>
- </ul>
- </div>
- <!-- Add font panel -->
- <div id="panel" class="fixed top-20 right-0 bg-base-100 p-5 shadow">
- <div class="panel-tile">
- <form @submit.prevent="AddTag">
- <div class="label label-xs">
- <span class="label-text label-xs">Add Tag</span>
- </div>
- <div class="join">
- <input class="join-item input input-xs input-bordered w-full max-w-xs" v-model="newTag" required placeholder="/Expressive/Funky">
- <button class="join-item btn btn-xs">Add</button>
- </div>
- </form>
- <div class="divider"></div>
- <form @submit.prevent="AddFamily">
- <div class="label lavel-xs">
- <span class="label-text label-xs">Add Family</span>
- </div>
- <div class="join">
- <input class="join-item input input-xs input-bordered w-full max-w-xs" list="items" v-model="newFamily" required placeholder="Family Name">
- <datalist id="items">
- <option v-for="family in uniqueFamilies" :value="family">
- </datalist>
- <input class="join-item input input-xs input-bordered btn-square" v-model="newWeight" required placeholder="Score">
- <button class="join-item btn btn-xs">Add</button>
- </div>
- </form>
- </div>
- <div class="divider"></div>
- <div class="panel-tile" style="max-height: 100px; overflow: scroll;">
- <label>History</label>
- <div v-if="isEdited">
- <p style="font-size: 10pt;" v-for="item in history">{{ item }}</p>
- </div>
- <div v-else>
- <p style="font-size: 10pt;">No changes</p>
- </div>
- </div>
- </div>
- <div v-if="sortedFamilies.length === 0">
- <p>No families found for this tag. Please add some</p>
- </div>
- <div class="mt-20">
- <div class="item" v-for="family in sortedFamilies" :key="family.Family">
- <family-item :family="family" :ready="ready" @edited="edited" @remove="removeFamily"></family-item>
- </div>
- </div>
- </div>
- </body>
- <script>
- Vue.component('family-item', {
- props: ['family', 'ready'],
- template: `
- <div class="item p-1">
- <div class="join">
- <b class="pr-2">{{ family.Family }}</b>
- <input style="width: 3rem;" class="join-item input input-xs input-bordered btn-square" v-model.lazy="family.Weight" @change="edited" placeholder="family.Weight">
- <button class="btn btn-xs join-item pr-2" @click="removeFamily">X</button>
- </div>
- <div v-if="ready" :style="familyStyle" contenteditable="true">
- {{ familyPangram }}
- </div>
- <div v-else>
- Loading...
- </div>
- <div class="divider"></div>
- </div>
- `,
- methods: {
- edited() {
- this.$emit('edited', this.family);
- },
- removeFamily() {
- this.$emit('remove', this.family);
- }
- },
- computed: {
- familyPangram() {
- return this.$root.familyPangram(this.family);
- },
- familyStyle() {
- return `font-family: "${this.family.Family}", "Adobe NotDef"; font-size: 32pt;`;
- }
- }
- });
- var app = new Vue({
- el: '#app',
- data() {
- return {
- ready: false,
- isEdited: false,
- commit: "refs/heads/main",
- newTag: "",
- newFamily: '',
- newWeight: '',
- CurrentCategory: "/Expressive/Calm",
- Categories: new Set(),
- Families: [],
- Seen: new Set(),
- Pangrams: new Map([
- ["English", "The quick brown fox jumps over the lazy dog."],
- ["Greek", "Ζαφείρι δέξου πάγκαλο, βαθῶν ψυχῆς τὸ σῆμα"],
- ["Cyrillic", "В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!"],
- ["Japanese", "いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす(ん"],
- ["Chinese", "視野無限廣,窗外有藍天"],
- ["Arabic", "نص حكيم له سر قاطع وذو شأن عظيم مكتوب على ثوب أخضر ومغلف بجلد أزرق"],
- ["Hebrew", "שפן אכל קצת גזר בטעם חסה, ודי."],
- ["Devanagari", "ऋषियों को सताने वाले दुष्ट राक्षसों के राजा रावण का सर्वनाश करने वाले विष्णुवतार भगवान श्रीराम, अयोध्या के महाराज दशरथ के बड़े सपुत्र थे।"],
- ["Bengali", "যেহেতু মানব পরিবারের সকল সদস্যের সমান ও অবিচ্ছেদ্য অধিকারসমূহ"],
- ["Gujarati", "કેમ કે માનવકુટુંબના દરેક સભ્યની પરંપરાપ્રાપ્ત પ્રતિષ્ઠાને અને"],
- ["Telugu", "మానవకుటంబమునందలి వ్యక్తులందరికిని గల ఆజన్మసిద్ధమైన ప్రతిపత్తిని"],
- ["Kannada", "ಎಲ್ಲಾ ಮಾನವರೂ ಸ್ವತಂತ್ರರಾಗಿಯೇ ಜನಿಸಿದ್ದಾರೆ. ಹಾಗೂ ಘನತೆ ಮತ್ತು ಹಕ್ಕುಗಳಲ್ಲಿ"],
- ["Khmer", "ដោយយល់ឃើញថា ការទទួលស្គាល់សេចក្ដីថ្លៃថ្នូរជាប់ពីកំណើត និងសិទ្ធិស្មើភាពគ្នា"],
- ["Phags Pa", "ꡗ ꡈꡱ ᠂ ꡒ ꡂ ꡈꡞ ᠂ ꡚꡖꡋ ꡈꡞꡋꡨꡖ ꡗꡛꡧꡖ ꡈꡋ ꡈꡱꡨꡖ ꡳꡬꡖ"],
- ["Tamil", "மனிதக் குடும்பத்தினைச் சேர்ந்த யாவரதும் உள்ளார்ந்த"],
- ]),
- FamilyScripts: new Map(),
- history: [],
- };
- },
- watch: {
- commit(newCommit) {
- this.updateURL();
- },
- CurrentCategory(newCategory) {
- this.updateURL();
- },
- },
- created() {
- this.loadCSV();
- this.loadFamilyPangrams();
- },
- mounted() {
- const urlParams = new URLSearchParams(window.location.search);
- const category = urlParams.get('category');
- if (category) {
- this.CurrentCategory = category;
- }
- const commit = urlParams.get('commit');
- if (commit) {
- this.commit = commit;
- this.loadCSV();
- }
- },
- computed: {
- sortedFamilies() {
- let ll = this.Families;
- let filtered = ll.filter(family => family["Group/Tag"] === this.CurrentCategory);
- filtered.sort(function(a, b) {return b.Weight - a.Weight;});
- return filtered;
- },
- uniqueFamilies() {
- return Array.from(new Set(this.Families.map((family) => family.Family)));
- }
- },
- methods: {
- sortedCategories() {
- return Array.from(this.Categories).sort();
- },
- updateURL() {
- const url = new URL(window.location);
- if (this.commit && this.commit !== "refs/heads/main") {
- url.searchParams.set('commit', this.commit);
- } else {
- url.searchParams.delete('commit');
- }
- if (this.CurrentCategory) {
- url.searchParams.set('category', this.CurrentCategory);
- } else {
- url.searchParams.delete('category');
- }
- history.pushState(null, '', url);
- },
- familyPangram(family) {
- return this.Pangrams.get(this.FamilyScripts.get(family.Family));
- },
- edited(family) {
- this.isEdited = true;
- this.history.push(`* ${family.Family},${family["Group/Tag"]},${family.Weight}`);
- },
- parseUnicode(str) {
- let ranges = str.split(",");
- let script = "English";
- let scripts = {
- "U+600-6FF": "Arabic",
- "U+900-97F": "Devanagari",
- "U+590-5FF": "Hebrew",
- "U+A80-AFF": "Gujarati",
- "U+C00-C7F": "Telugu",
- "U+C80-CFF": "Kannada",
- "U+980-9FE": "Bengali",
- "U+1780-17FF": "Khmer",
- "U+A840-A877": "Phags Pa",
- "U+0B82-0BFA": "Tamil",
- }
- for (let i = 0; i < ranges.length; i++) {
- for (let key in scripts) {
- if (ranges[i].includes(key)) {
- script = scripts[key];
- break;
- }
- }
- }
- return script;
- },
- async loadFamilyPangrams(delay = 1000) {
- await document.fonts.ready;
- let result = new Map();
- let fonts = document.fonts;
- fonts.forEach((font) => {
- if (!result.has(font.family)) {
- result.set(font.family, this.parseUnicode(font.unicodeRange));
- }
- });
- console.log(result.size)
- if (result.size < 1000) {
- console.log("retry")
- setTimeout(() => this.loadFamilyPangrams(), delay);
- }
- this.FamilyScripts = result;
- this.ready = true;
- },
- familyLink(Family) {
- return "https://fonts.googleapis.com/css2?family=" + Family.replace(" ", "+") + "&display=swap"
- },
- familyCSSClass(Family) {
- let cssName = Family.family.replace(" ", "-").toLowerCase();
- return `.${cssName} {
- font-family: "${Family.family}", sans-serif;
- font-weight: 400;
- font-style: normal;
- }`
- },
- familySelector(Family) {
- let cssName = Family.Family.replace(" ", "-").toLowerCase();
- return cssName;
- },
- familyStyle(Family) {
- return `font-family: "${Family.Family}", "Adobe NotDef"; font-size: 32pt;`
- },
- AddTag() {
- this.isEdited = true;
- this.Categories.add(this.newTag);
- this.history.push(`+ Tag added "${this.newTag}"`);
- this.CurrentCategory = this.newTag;
- },
- AddFamily() {
- this.isEdited = true;
- let newFamily = { Weight: this.newWeight, Family: this.newFamily, "Group/Tag": this.CurrentCategory }
- let tagKey = `${newFamily.Family},${newFamily["Group/Tag"]}`;
- if (this.Seen.has(tagKey)) {
- alert(`Tag "${newFamily.Family}" already exists in "${this.CurrentCategory}"`);
- return;
- }
- this.Families.push(newFamily);
-
- this.history.push(`+ ${newFamily.Family},${newFamily["Group/Tag"]},${newFamily.Weight}`);
- },
- AddPlaceHolderTags() {
- this.isEdited = true;
- const existingTags = this.sortedFamilies
- let seen = new Set();
- existingTags.forEach((family) => seen.add(family.Family));
- const familiesToAdd = this.uniqueFamilies
- familiesToAdd.forEach((family) => {
- if (!seen.has(family)) {
- this.Families.push({ Family: family, "Group/Tag": this.CurrentCategory, Weight: "" });
- }
- });
- this.history.push(`+ Placeholder tags added for ${this.CurrentCategory}`);
- },
- RemovePlaceHolderTags() {
- this.isEdited = true;
- this.Families = this.Families.filter((family) => family.Weight !== "");
- this.history.push(`- Placeholder tags removed for all categories`);
- },
- removeFamily(Family) {
- this.isEdited = true;
- this.Families = this.Families.filter((t) => t !== Family);
- this.history.push(`- ${Family.Family},${Family["Group/Tag"]},${Family.Weight}`);
- },
- familiesToCSV() {
- this.RemovePlaceHolderTags();
- this.Families = this.Families.filter((t) => t.Family !== "");
- // The sorting function used is case sensitive.
- // This means that "A" will come before "a".
- this.Families = Array.from(this.Families).sort((a, b) => {
- if (`${a.Family},${a['Group/Tag']}` < `${b.Family},${b['Group/Tag']}`) {
- return -1;
- }
- if (`${a.Family},${a['Group/Tag']}` > `${b.Family},${b['Group/Tag']}`) {
- return 1;
- }
- return 0;
- });
- // Include a newline at the end to keep Evan's Vim happy.
- return Papa.unparse(this.Families,
- {
- columns: ["Family", "Group/Tag", "Weight"],
- skipEmptyLines: true,
- header: false
- }
- ) + "\n";
- },
- saveCSV() {
- let csv = this.familiesToCSV();
- const blob = new Blob([csv], { type: 'text/csv' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = "families.csv";
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- },
- prCSV() {
- let csv = this.familiesToCSV();
- 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.");
- navigator.clipboard.writeText(csv);
- window.open("https://github.com/google/fonts/edit/main/tags/all/families.csv")
- },
- loadCSV() {
- if (this.history.length > 0) {
- let proceed = confirm("Checking out a new commit will delete any changes you've made. Would you like to continue?")
- if (proceed === false) {
- return;
- }
- }
- this.history = [];
- const csvFilePath = `https://raw.githubusercontent.com/google/fonts/${this.commit}/tags/all/families.csv`; // Update this path to your CSV file
- fetch(csvFilePath)
- .then(response => {
- if (response.status === 404) {
- alert(`No data found for commit "${this.commit}". Please input a git commit hash e.g 538d9635c160306b40af31c9a3821c59b285bbff`);
- }
- return response.text()
- })
- .then(csvText => {
- csvText = "Family,Group/Tag,Weight\r\n" + csvText;
- Papa.parse(csvText, {
- header: true,
- complete: (results) => {
- this.Categories = new Set(results.data.map((row) => row["Group/Tag"]));
- results.data.map((row) => {
- this.Seen.add(`${row.Family},${row["Group/Tag"]}`);
- this.Families.push(
- {
- Family: row.Family,
- "Group/Tag": row["Group/Tag"],
- Weight: parseInt(row.Weight, 10)
- }
- )
- });
- this.Families = results.data.map((row) => ({
- Weight: row.Weight,
- Family: row.Family,
- "Group/Tag": row["Group/Tag"]
- })
- );
- }
- });
- })
- .catch(error => {
- console.error('Error loading CSV file:', error);
- });
- }
- }
- } // methods
- )
- // close open navbar dropdowns when user clicks elsewhere
- var details = [...document.querySelectorAll('details')];
- document.addEventListener('click', function(e) {
- if (!details.some(f => f.contains(e.target))) {
- details.forEach(f => f.removeAttribute('open'));
- } else {
- details.forEach(f => !f.contains(e.target) ? f.removeAttribute('open') : '');
- }
- })
- </script>
- <style>
- @font-face {
- font-family: "Adobe NotDef";
- src: url(https://cdn.jsdelivr.net/gh/adobe-fonts/adobe-notdef/AND-Regular.ttf);
- }
- </style>
|