sfc-utils 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+
3
+ var { google } = require("googleapis");
4
+ var opn = require("opn");
5
+
6
+ var http = require("http");
7
+ var os = require("os");
8
+ var path = require("path");
9
+ var url = require("url");
10
+ var fs = require("fs");
11
+ var writeFile = require('write');
12
+
13
+ // Prep the service account for drive
14
+ var serviceAccountCreds = path.join(os.homedir(), "service-account-google-creds.json");
15
+ var tokenLocation = path.join(os.homedir(), ".google_oauth_token");
16
+
17
+ var fallbackAuth = function() {
18
+ // If it's coming from EC2, pull from project
19
+ if (process.env.GOOGLE_OAUTH_SYSTEM === "EC2"){
20
+ tokenLocation = "../.google_oauth_token";
21
+ }
22
+
23
+ try {
24
+ var tokens = fs.readFileSync(tokenLocation, "utf-8");
25
+ tokens = JSON.parse(tokens);
26
+ auth = new google.auth.OAuth2(process.env.GOOGLE_OAUTH_CLIENT_ID, process.env.GOOGLE_OAUTH_CONSUMER_SECRET);
27
+ auth.setCredentials(tokens);
28
+
29
+ auth.on("tokens", function(update) {
30
+ Object.assign(tokens, update);
31
+ fs.writeFileSync(tokenLocation, JSON.stringify(tokens, null, 2));
32
+ });
33
+ } catch(err){
34
+ // If we error here, fire up local (as long as we're on on EC2)
35
+ if (process.env.GOOGLE_OAUTH_SYSTEM !== "EC2"){
36
+ task()
37
+ }
38
+ }
39
+ return auth;
40
+ }
41
+
42
+ var authenticate = function({fallback}) {
43
+ if (fallback){
44
+ return new Promise((resolve, reject) => {
45
+ console.log("Service account failed, falling back to regular token (to use the service account, share this sheet or doc with sfchronicle-gatsby@zinc-proton-250521.iam.gserviceaccount.com)")
46
+ resolve(fallbackAuth());
47
+ })
48
+ }
49
+ // Try to use the service account first
50
+ return new Promise((resolve, reject) => {
51
+ try {
52
+ // If it's coming from EC2, pull from project
53
+ if (process.env.GOOGLE_OAUTH_SYSTEM === "EC2"){
54
+ serviceAccountCreds = "../service-account-google-creds.json";
55
+ }
56
+
57
+ var serviceAccountJSON = fs.readFileSync(serviceAccountCreds, "utf-8");
58
+ serviceAccountJSON = JSON.parse(serviceAccountJSON);
59
+
60
+ // configure a JWT auth client
61
+ let jwtClient = new google.auth.JWT(
62
+ serviceAccountJSON.client_email,
63
+ null,
64
+ serviceAccountJSON.private_key,
65
+ [
66
+ 'https://www.googleapis.com/auth/spreadsheets',
67
+ 'https://www.googleapis.com/auth/drive'
68
+ ]
69
+ );
70
+ //authenticate request
71
+ jwtClient.authorize(function (err, tokens) {
72
+ if (err) {
73
+ console.log("Stage 1 error, fallback auth");
74
+ resolve(fallbackAuth());
75
+ } else {
76
+ console.log("Successfully connected to service account!");
77
+ // Return the jwtClient as auth
78
+ resolve(jwtClient);
79
+ }
80
+ });
81
+ } catch (err){
82
+ // It's ok if it errors, we have the fallback
83
+ console.log("Stage 2 error, fallback auth");
84
+ resolve(fallbackAuth());
85
+ }
86
+ })
87
+ };
88
+
89
+ var task = function() {
90
+ // var done = this.async();
91
+
92
+ var clientID = process.env.GOOGLE_OAUTH_CLIENT_ID;
93
+ var secret = process.env.GOOGLE_OAUTH_CONSUMER_SECRET;
94
+
95
+ var client = new google.auth.OAuth2(clientID, secret, "http://localhost:8000/authenticate/");
96
+ google.options({
97
+ auth: client
98
+ });
99
+
100
+ var scopes = [
101
+ "https://www.googleapis.com/auth/drive",
102
+ "https://www.googleapis.com/auth/spreadsheets"
103
+ ];
104
+
105
+ var authURL = client.generateAuthUrl({
106
+ access_type: "offline",
107
+ scope: scopes.join(" "),
108
+ prompt: "consent"
109
+ });
110
+
111
+ var onRequest = function(request, response) {
112
+ response.setHeader("Connection", "close");
113
+ if (request.url.indexOf("authenticate") > -1) {
114
+ return onAuthenticated(request, response);
115
+ } else if (request.url.indexOf("authorize") > -1) {
116
+ response.setHeader("Location", authURL);
117
+ response.writeHead(302);
118
+ } else {
119
+ response.writeHead(404);
120
+ }
121
+ response.end();
122
+ };
123
+
124
+ var onAuthenticated = async function(request, response) {
125
+ var requestURL = request.url[0] == "/" ? "localhost:8000" + request.url : request.url;
126
+ var query = new url.URL(requestURL).searchParams;
127
+ var code = query.get("code");
128
+ if (!code) return;
129
+ try {
130
+ var token = await client.getToken(code);
131
+ var tokens = token.tokens;
132
+ writeFile(tokenLocation, JSON.stringify(tokens, null, 2), function(err) {
133
+ if (err) {
134
+ console.log(err);
135
+ } else {
136
+ console.log("Authenticated");
137
+ }
138
+ });
139
+ response.end("Done! Now run your command again.");
140
+ } catch (err) {
141
+ response.end(err);
142
+ }
143
+ };
144
+
145
+ var server = http.createServer(onRequest);
146
+ server.listen(8000, () => opn("http://localhost:8000/authorize"));
147
+ }
148
+
149
+ let fullAuth = {
150
+ task: task,
151
+ authenticate: authenticate
152
+ }
153
+ module.exports = fullAuth;
package/copy/sheets.js ADDED
@@ -0,0 +1,151 @@
1
+ /*
2
+ * Uses the Google Sheets API to pull data from Sheets and load it onto shared
3
+ * state. Writes the data out to JSON for later reference. Does not currently
4
+ * check for existing data to merge--it does a fresh pull every time.
5
+ * Sheets must be shared with the service account email before they can be accessed with this task
6
+ * @param {object} project standard object from project-config.json or project.json
7
+ * @param {string} directory optional alternate path to directory in which to save the output
8
+
9
+ */
10
+
11
+ var { google } = require("googleapis");
12
+ var api = google.sheets("v4");
13
+ var writeFile = require("write");
14
+ var authObj = require("./googleauth");
15
+
16
+ var cast = function (str, forceStr) {
17
+ if (!forceStr){
18
+ if (typeof str !== "string") {
19
+ if (typeof str.value == "string") {
20
+ str = str.value;
21
+ } else {
22
+ return str;
23
+ }
24
+ }
25
+ if (str.match(/^-?(0?\.|[1-9])[\d\.]*$/) || str == "0") {
26
+ var n = Number(str);
27
+ if (isNaN(n)) return str;
28
+ return n;
29
+ }
30
+ if (str.toLowerCase() == "true" || str.toLowerCase() == "false") {
31
+ return str.toLowerCase() == "true" ? true : false;
32
+ }
33
+ }
34
+ // To force string, just return string
35
+
36
+ return str;
37
+ };
38
+
39
+ let googleAuth = (project, directory = null, forceStr = false) => {
40
+ var auth = null;
41
+ authObj
42
+ .authenticate({ fallback: false })
43
+ .then((resp) => {
44
+ auth = resp;
45
+ grabSheets(auth, project, directory, forceStr).catch(() => {
46
+ // If the first attempt failed, then make another req using the fallback
47
+ authObj.authenticate({ fallback: true }).then((resp) => {
48
+ auth = resp;
49
+ grabSheets(auth, project, directory, forceStr);
50
+ });
51
+ });
52
+ })
53
+ .catch(() => {
54
+ // Failure if we fall back but there's no token
55
+ auth = authObj.task();
56
+ grabSheets(auth, project, directory, forceStr);
57
+ });
58
+ };
59
+
60
+ let grabSheets = (auth, project, directory, forceStr) => {
61
+ return new Promise((resolveAll, rejectAll) => {
62
+ var sheetKeys = project.GOOGLE_SHEETS;
63
+ if (!sheetKeys){
64
+ // Try the old way
65
+ sheetKeys = project.sheets;
66
+ }
67
+
68
+ if (!sheetKeys || !sheetKeys.length) {
69
+ console.log(
70
+ "You must specify a spreadsheet key in project.json or auth.json!"
71
+ );
72
+ return false;
73
+ }
74
+
75
+ let promiseStack = [];
76
+ for (var spreadsheetId of sheetKeys) {
77
+ let promiseItem = new Promise((resolve, reject) => {
78
+ getSheet(resolve, reject, auth, spreadsheetId, directory, forceStr);
79
+ });
80
+ promiseStack.push(promiseItem);
81
+ }
82
+
83
+ Promise.all(promiseStack)
84
+ .then(() => {
85
+ // Resolve the whole thing
86
+ resolveAll();
87
+ })
88
+ .catch(() => {
89
+ // If this was triggered, reject all to fall back
90
+ rejectAll();
91
+ });
92
+ });
93
+ };
94
+
95
+ let getSheet = async (resolve, reject, auth, spreadsheetId, directory, forceStr) => {
96
+ let output = await api.spreadsheets
97
+ .get({
98
+ auth,
99
+ spreadsheetId,
100
+ })
101
+ .catch(() => {
102
+ // This might fail if we don't have access
103
+ reject();
104
+ });
105
+ if (!output) {
106
+ return;
107
+ }
108
+ var book = output.data;
109
+ var { sheets, spreadsheetId } = book;
110
+ for (var sheet of sheets) {
111
+ if (sheet.properties.title[0] == "_") continue;
112
+ var response = await api.spreadsheets.values.get({
113
+ auth,
114
+ spreadsheetId,
115
+ range: `${sheet.properties.title}!A:AAA`,
116
+ majorDimension: "ROWS",
117
+ });
118
+ var { values } = response.data;
119
+ var header = values.shift();
120
+ var isKeyed = header.indexOf("key") > -1;
121
+ var isValued = header.indexOf("value") > -1;
122
+ var out = isKeyed ? {} : [];
123
+ for (var row of values) {
124
+ // skip blank rows
125
+ if (!row.length) continue;
126
+ var obj = {};
127
+ row.forEach(function (value, i) {
128
+ var key = header[i];
129
+ obj[key] = cast(value, forceStr);
130
+ });
131
+ if (isKeyed) {
132
+ out[obj.key] = isValued ? obj.value : obj;
133
+ } else {
134
+ out.push(obj);
135
+ }
136
+ }
137
+
138
+ //set alternate dir if we have it
139
+ directory = directory || "src/data/";
140
+ var file_path = `${directory}${sheet.properties.title.replace(
141
+ /\s+/g,
142
+ "_"
143
+ )}.sheet.json`;
144
+ console.log(`Saving sheet to ${file_path}`);
145
+ // grunt.file.write(filename, JSON.stringify(out, null, 2));
146
+ writeFile(file_path, JSON.stringify(out, null, 2));
147
+ resolve(file_path);
148
+ }
149
+ };
150
+
151
+ module.exports = { googleAuth };
@@ -0,0 +1,45 @@
1
+ // There are additional weights and versions of these fonts available,
2
+ // but adding them makes the client download larger
3
+ @font-face {
4
+ font-family: 'ChronicleDispCond-Black';
5
+ font-display: swap;
6
+ font-style: normal;
7
+ font-weight: 400;
8
+ src: local("ChronicleDispCond-Black"),url("https://www.timesunion.com/css/core/fonts/chronicle/ChronicleDispCond-Black_Web.woff2") format("woff2"), url("https://www.timesunion.com/css/core/fonts/chronicle/ChronicleDispCond-Black_Web.woff") format("woff");
9
+ }
10
+ @font-face {
11
+ font-family: 'ChronicleDispCond-Roman';
12
+ font-display: swap;
13
+ font-style: normal;
14
+ font-weight: 400;
15
+ src: local('ChronicleDispCond-Roman'), url("https://www.timesunion.com/css/core/fonts/chronicle/ChronicleDispCond-Roman_Web.woff2") format('woff2'), url("https://www.timesunion.com/css/core/fonts/chronicle/ChronicleDispCond-Roman_Web.woff") format('woff');
16
+ }
17
+ @font-face {
18
+ font-family: 'ChronicleTextG2-Roman';
19
+ font-display: swap;
20
+ font-style: normal;
21
+ font-weight: 400;
22
+ src: local('ChronicleTextG2-Roman'), url("https://www.timesunion.com/css/core/fonts/chronicle/ChronicleTextG2-Roman_Web.woff2") format('woff2'), url("https://www.timesunion.com/css/core/fonts/chronicle/ChronicleTextG2-Roman_Web.woff") format('woff');
23
+ }
24
+ @font-face {
25
+ font-family: 'ChronicleTextG2-Bold';
26
+ font-display: swap;
27
+ font-style: normal;
28
+ font-weight: 400;
29
+ src: local('ChronicleTextG2-Bold'), url("https://www.timesunion.com/css/core/fonts/chronicle/ChronicleTextG2-Bold_Web.woff2") format('woff2'), url("https://www.timesunion.com/css/core/fonts/chronicle/ChronicleTextG2-Bold_Web.woff") format('woff');
30
+ }
31
+ @font-face {
32
+ font-family: 'HelveticaNeue-Roman';
33
+ font-display: swap;
34
+ font-style: normal;
35
+ font-weight: 400;
36
+ src: local('HelveticaNeue-Roman'), url("https://www.timesunion.com/css/core/fonts/neuehelvetica/HelveticaNeue-Roman.woff2") format('woff2'), url("https://www.timesunion.com/css/core/fonts/neuehelvetica/HelveticaNeue-Roman.woff") format('woff');
37
+ }
38
+ @font-face {
39
+ font-family: 'HelveticaNeue-HeavyCond';
40
+ font-display: swap;
41
+ font-style: normal;
42
+ font-weight: 400;
43
+ src: local('HelveticaNeue-HeavyCond'), url("https://www.timesunion.com/css/core/fonts/neuehelvetica/HelveticaNeue-HeavyCond.woff2") format('woff2'), url("https://www.timesunion.com/css/core/fonts/neuehelvetica/HelveticaNeue-HeavyCond.woff") format('woff');
44
+ }
45
+
@@ -0,0 +1,50 @@
1
+ // The brand fonts
2
+ @font-face {
3
+ font-family: 'Lora Bold';
4
+ font-display: swap;
5
+ font-style: normal;
6
+ font-weight: 400;
7
+ src: local('Lora Bold'), url("https://www.sfchronicle.com/css/core/fonts/lora/lora-v12-latin-700.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/lora/lora-v12-latin-700.woff") format('woff');
8
+ }
9
+ @font-face {
10
+ font-family: 'Lora Regular';
11
+ font-display: swap;
12
+ font-style: normal;
13
+ font-weight: 400;
14
+ src: local('Lora Regular'), url("https://www.sfchronicle.com/css/core/fonts/lora/lora-v12-latin-regular.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/lora/lora-v12-latin-regular.woff") format('woff');
15
+ }
16
+ @font-face {
17
+ font-family: 'Lora Light';
18
+ font-display: swap;
19
+ font-style: normal;
20
+ font-weight: 300;
21
+ src: local('Lora Light'), url("https://www.sfchronicle.com/css/core/fonts/lora/lora-v12-latin-regular.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/lora/lora-v12-latin-regular.woff") format('woff');
22
+ }
23
+ @font-face {
24
+ font-family: 'Source Sans Pro Light';
25
+ font-display: swap;
26
+ font-style: normal;
27
+ font-weight: 300;
28
+ src: local('Source Sans Pro Regular'), url("https://www.sfchronicle.com/css/core/fonts/sourcesans/source-sans-pro-v11-latin-300.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/sourcesans/source-sans-pro-v11-latin-300.woff") format('woff');
29
+ }
30
+ @font-face {
31
+ font-family: 'Source Sans Pro Regular';
32
+ font-display: swap;
33
+ font-style: normal;
34
+ font-weight: 400;
35
+ src: local('Source Sans Pro Regular'), url("https://www.sfchronicle.com/css/core/fonts/sourcesans/source-sans-pro-v11-latin-regular.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/sourcesans/source-sans-pro-v11-latin-regular.woff") format('woff');
36
+ }
37
+ @font-face {
38
+ font-family: 'Source Sans Pro Semibold';
39
+ font-display: swap;
40
+ font-style: normal;
41
+ font-weight: 600;
42
+ src: local('Source Sans Pro Semibold'), url("https://www.sfchronicle.com/css/core/fonts/sourcesans/source-sans-pro-v11-latin-600.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/sourcesans/source-sans-pro-v11-latin-600.woff") format('woff');
43
+ }
44
+ @font-face {
45
+ font-family: 'Source Sans Pro Bold';
46
+ font-display: swap;
47
+ font-style: normal;
48
+ font-weight: 700;
49
+ src: local('Source Sans Pro Bold'), url("https://www.sfchronicle.com/css/core/fonts/sourcesans/source-sans-pro-v11-latin-700.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/sourcesans/source-sans-pro-v11-latin-700.woff") format('woff');
50
+ }
@@ -0,0 +1,51 @@
1
+ // There are additional weights and versions of these fonts available,
2
+ // but adding them makes the client download larger
3
+ @font-face {
4
+ font-family: 'Marr Sans Semibold';
5
+ font-display: swap;
6
+ font-style: normal;
7
+ font-weight: 400;
8
+ src: local('Marr Sans Semibold'), url("https://www.houstonchronicle.com/css/core/fonts/marrsans/MarrSans-Semibold-Web.woff2") format('woff2'), url("https://www.houstonchronicle.com/css/core/fonts/marrsans/MarrSans-Semibold-Web.woff") format('woff');
9
+ }
10
+ @font-face {
11
+ font-family: 'Marr Sans Regular';
12
+ font-display: swap;
13
+ font-style: normal;
14
+ font-weight: 400;
15
+ src: local('Marr Sans Regular'), url("https://www.houstonchronicle.com/css/core/fonts/marrsans/MarrSans-Regular-Web.woff2") format('woff2'), url("https://www.houstonchronicle.com/css/core/fonts/marrsans/MarrSans-Regular-Web.woff") format('woff');
16
+ }
17
+ @font-face {
18
+ font-family: 'Marr Sans Condensed Medium';
19
+ font-display: swap;
20
+ font-style: normal;
21
+ font-weight: 400;
22
+ src: local("Marr Sans Condensed Medium"),url("https://www.houstonchronicle.com/css/core/fonts/marrsans/MarrSansCondensed-Medium-Web.woff2") format("woff2"), url("https://www.houstonchronicle.com/css/core/fonts/marrsans/MarrSansCondensed-Medium-Web.woff") format("woff");
23
+ }
24
+ @font-face {
25
+ font-family: 'Marr Sans Condensed Semibold';
26
+ font-display: swap;
27
+ font-style: normal;
28
+ font-weight: 400;
29
+ src: local("Marr Sans Condensed Semibold"),url("https://www.houstonchronicle.com/css/core/fonts/marrsans/MarrSansCondensed-Semibold-Web.woff2") format("woff2"), url("https://www.houstonchronicle.com/css/core/fonts/marrsans/MarrSansCondensed-Semibold-Web.woff") format("woff");
30
+ }
31
+ @font-face {
32
+ font-family: 'Publico Text Roman';
33
+ font-display: swap;
34
+ font-style: normal;
35
+ font-weight: 400;
36
+ src: local('Publico Text Roman'), url("https://www.houstonchronicle.com/css/core/fonts/publico/PublicoText-Roman-Web.woff2") format('woff2'), url("https://www.houstonchronicle.com/css/core/fonts/publico/PublicoText-Roman-Web.woff") format('woff');
37
+ }
38
+ @font-face {
39
+ font-family: 'Publico Text Bold';
40
+ font-display: swap;
41
+ font-style: normal;
42
+ font-weight: 400;
43
+ src: local('Publico Text Bold'), url("https://www.houstonchronicle.com/css/core/fonts/publico/PublicoText-Bold-Web.woff2") format('woff2'), url("https://www.houstonchronicle.com/css/core/fonts/publico/PublicoText-Bold-Web.woff") format('woff');
44
+ }
45
+ @font-face {
46
+ font-family: 'Publico Headline Medium';
47
+ font-display: swap;
48
+ font-style: normal;
49
+ font-weight: 400;
50
+ src: local('Publico Headline Medium'), url("https://www.houstonchronicle.com/css/core/fonts/publico/PublicoHeadline-Medium-Web.woff2") format('woff2'), url("https://www.houstonchronicle.com/css/core/fonts/publico/PublicoHeadline-Medium-Web.woff") format('woff');
51
+ }
package/fonts/sfc.less ADDED
@@ -0,0 +1,57 @@
1
+ // The brand fonts
2
+ @font-face {
3
+ font-family: 'National Bold';
4
+ font-display: swap;
5
+ font-style: normal;
6
+ font-weight: 400;
7
+ src: local('National Bold'), url("https://www.sfchronicle.com/css/core/fonts/national/NationalWeb-Bold.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/national/NationalWeb-Bold.woff") format('woff');
8
+ }
9
+ @font-face {
10
+ font-family: 'National Medium';
11
+ font-display: swap;
12
+ font-style: normal;
13
+ font-weight: 400;
14
+ src: local('National Medium'), url("https://www.sfchronicle.com/css/core/fonts/national/NationalWeb-Medium.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/national/NationalWeb-Medium.woff") format('woff');
15
+ }
16
+ @font-face {
17
+ font-family: 'National Light';
18
+ font-display: swap;
19
+ font-style: normal;
20
+ font-weight: 400;
21
+ src: local('National Light'), url("https://www.sfchronicle.com/css/core/fonts/national/NationalWeb-Light.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/national/NationalWeb-Light.woff") format('woff');
22
+ }
23
+ @font-face {
24
+ font-family: 'National Book';
25
+ font-display: swap;
26
+ font-style: normal;
27
+ font-weight: 400;
28
+ src: local('National Book'), url("https://www.sfchronicle.com/css/core/fonts/national/NationalWeb-Book.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/national/NationalWeb-Book.woff") format('woff');
29
+ }
30
+ @font-face {
31
+ font-family: 'Tiempos Regular';
32
+ font-display: swap;
33
+ font-style: normal;
34
+ font-weight: 400;
35
+ src: local('Tiempos Regular'), url("https://www.sfchronicle.com/css/core/fonts/tiempos/TiemposTextWeb-Regular.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/tiempos/TiemposTextWeb-Regular.woff") format('woff');
36
+ }
37
+ @font-face {
38
+ font-family: 'Tiempos Bold';
39
+ font-display: swap;
40
+ font-style: normal;
41
+ font-weight: 400;
42
+ src: local('Tiempos Bold'), url("https://www.sfchronicle.com/css/core/fonts/tiempos/TiemposTextWeb-Bold.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/tiempos/TiemposTextWeb-Bold.woff") format('woff');
43
+ }
44
+ @font-face {
45
+ font-family: 'Tiempos Headline Light';
46
+ font-display: swap;
47
+ font-style: normal;
48
+ font-weight: 400;
49
+ src: local('Tiempos Headline Light'), url("https://www.sfchronicle.com/css/core/fonts/tiempos/TiemposHeadlineWeb-Light.woff2") format('woff2'), url("https://www.sfchronicle.com/css/core/fonts/tiempos/TiemposHeadlineWeb-Light.woff") format('woff');
50
+ }
51
+ @font-face {
52
+ font-family: 'Tiempos Headline Black';
53
+ font-display: swap;
54
+ font-style: normal;
55
+ font-weight: 400;
56
+ src: local("Tiempos Headline Black"),url(https://www.sfchronicle.com/css/core/fonts/tiempos/TiemposHeadlineWeb-Black.woff2) format("woff2"), url("https://www.sfchronicle.com/css/core/fonts/tiempos/TiemposHeadlineWeb-Black.woff") format("woff");
57
+ }