/* Parse typography.com, find typeface file names. */ /* Missing some styles. Use https://www.typography.com/api/v1/product_lines/100054?include=multipurpose_styles,office_styles,screen_smart_styles. */ package main import ( "bufio" "encoding/json" "errors" "fmt" "io" "io/fs" "log" "net/http" "os" "regexp" "strconv" "strings" ) const ( base = "https://typography.com/api/v1" ) func request(url string) []byte { req, err := http.NewRequest("GET", url, nil) if err != nil { log.Fatal(err) } resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } rt, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } resp.Body.Close() return rt } func dlThenRead(name, url string) []byte { _, err := os.Stat(name) if errors.Is(err, fs.ErrNotExist) { log.Print(err) out, err := os.Create(name) if err != nil { log.Fatal(err) } defer out.Close() outWriter := bufio.NewWriter(out) _, err = outWriter.Write(request(url)) if err != nil { log.Fatal(err) } _ = outWriter.Flush() } else if err != nil { log.Fatal(err) } in, err := os.Open(name) if err != nil { log.Fatal(err) } defer in.Close() inReader := bufio.NewReader(in) data, err := io.ReadAll(inReader) if err != nil { log.Fatal(err) } return data } func parseAndPrint(shortName, css string) { /* Grep for lines containing CSS rules with font names, */ for _, line := range strings.Split(css, "\n") { match, err := regexp.Match("font-family:.*", []byte(line)) if err != nil { log.Fatal(err) } if match { /* Remove rule boilerplate. */ line = strings.Replace(line, " font-family: ", "", 1) line = strings.Replace(line, ", Fallback, Courier;", "", 1) /* Split list of file names. */ for _, style := range strings.Split(line, ", ") { /* TDB or TDA should become TD. Decided against the complexity of regexp for this. */ style = strings.Replace(style, "TDB", "TD", 1) style = strings.Replace(style, "TDA", "TD", 1) /* Each style has two files: one with most of the glpyhs, another with just the ' '. Both are necessary, and need merging later. */ fmt.Println(shortName, style) fmt.Println(shortName, style+"-Space") } } } } func traverse(name, shortName string, id string) { //fmt.Printf("%s %s %s\n", id, name, shortName) /* Create a directory for each typeface. The short name used in URLs contains no spaces. It works well for this use. */ _, err := os.Stat(shortName) if err != nil { err = os.Mkdir(shortName, 0666) if err != nil { log.Fatal(err) } } err = os.Chdir(shortName) defer os.Chdir("..") /* The long name of the typeface is worth keeping. */ _, err = os.Stat("name") if errors.Is(err, fs.ErrNotExist) { out, err := os.Create("name") if err != nil { log.Fatal(err) } defer out.Close() _, err = out.Write([]byte(name)) if err != nil { log.Fatal(err) } } /* Using the ID passed in from the initial JSON, retrieve new JSON. This contains details on the actual typeface. */ /* Parsing for OTF names in progress. */ detailsJson := dlThenRead(id, base+"/product_lines/"+id+ "?include=special_offers,multipurpose_styles,office_styles,screen_smart_styles&quantity=1&serializer=array") var details interface{} json.Unmarshal(detailsJson, &details) m := details.(map[string]interface{}) /* The important field is an alphabetic identifier. It must be used in the following step. Hoop jumping. */ alphaId := m["desktop_try_code"].(string) /* This JSON contains information about how the typeface should be layed out. Embedded is a set of CSS rules. The CSS rules contain the file names of each style of the typeface. */ layoutJson := dlThenRead(alphaId, base+"/try/layout/"+ alphaId) var layout interface{} json.Unmarshal(layoutJson, &layout) layoutMap := layout.(map[string]interface{}) /* Interesting data is a few layers deep. */ for _, mL2 := range layoutMap { for _, mL3 := range mL2.(map[string]interface{}) { switch mL3.(type) { case map[string]interface{}: mL4 := mL3.(map[string]interface{}) layoutL2Json := []byte(mL4["layout_data"].(string)) var layoutL2 interface{} json.Unmarshal(layoutL2Json, &layoutL2) /* Send the CSS for parsing. */ parseAndPrint(shortName, layoutL2.(map[string]interface{})["css"].(string)) case string: default: } } } /* OTF names. Base names are available as woff. */ otf := m["multipurpose_styles"].(map[string]interface{})["styles"].([]interface{}) for _, otf := range otf { style := otf.(map[string]interface{})["file_name"].(string) style = strings.TrimSuffix(style, ".otf") style = strings.Replace(style, "-SC", "-TD", 1) if !strings.Contains(style, "-TD") || strings.Contains(style,"_TD") || shortName == "whitney" { continue } fmt.Println(shortName, style) fmt.Println(shortName, style+"-Space") } } func main() { /* findByName serves as the index to each of the typefaces. If it doesn't exist, download it. */ _, err := os.Stat("findByName") if errors.Is(err, fs.ErrNotExist) { log.Print(err) out, err := os.Create("findByName") if err != nil { log.Fatal(err) } defer out.Close() outWriter := bufio.NewWriter(out) _, err = outWriter.Write(request(base + "/findByName?serializer=array")) if err != nil { log.Fatal(err) } _ = outWriter.Flush() } in, err := os.Open("findByName") if err != nil { log.Fatal(err) } defer in.Close() inReader := bufio.NewReader(in) indexJson, err := io.ReadAll(inReader) if err != nil { log.Fatal(err) } /* findByName is in JSON. */ var index interface{} err = json.Unmarshal(indexJson, &index) if err != nil { log.Fatal(err) } /* The actually interesting data is a few levels deep. */ m := index.(map[string]interface{}) for _, mL2 := range m { for _, mL3 := range mL2.([]interface{}) { arr := mL3.(map[string]interface{}) /* "name" is the full name, "slug" is the short name, "type_id" is the identifier used in the traversal. */ traverse(arr["name"].(string), arr["slug"].(string), strconv.Itoa(int(arr["type_id"].(float64)))) } } }