1const MAX_AGE = 1000 * 60 * 10; 2const CORS_PROXY = "https://cors-anywhere.herokuapp.com/" 3const getFeed = function(name) { 4 return `${CORS_PROXY}https://www.economist.com/${name}/rss.xml` 5} 6const feeds = [ 7 'britain', 8 'europe', 9 'united-states', 10 'the-americas', 11 'middle-east-and-africa', 12 'asia', 13 'china', 14] 15 16const synonyms = { 17 'republic of the congo': ['congo'], 18 'china': ['hong kong'], 19 'united states': ['america', 'trump', 'hollywood'], 20 'india': ['bollywood'], 21 'côte d\'ivoire': ['ivory coast'], 22 'viet nam': ['vietnam'], 23 'bolivia': ['evo morales'], 24} 25 26const storiesByCountry = {}; 27 28function initializeCountryData() { 29 countryList.forEach(c => { 30 c.data = countryData.find(d => parseInt(d.ccn3) === parseInt(c.id)); 31 if (!c.data) { 32 console.log("no data for ", c.id, c.name); 33 return 34 } 35 }); 36 countryList = countryList.filter(c => c.data); 37 countryList.forEach(c => { 38 c.name = c.data.name.common; 39 let lowerName = c.name.toLowerCase(); 40 let comma = lowerName.indexOf(',') 41 if (comma !== -1) { 42 lowerName = lowerName.substring(0, comma); 43 } 44 c.terms = [lowerName].concat(synonyms[lowerName] || []); 45 if (window.capitals[c.name]) { 46 c.terms.push(window.capitals[c.name].toLowerCase()); 47 } 48 let leader = window.leaders.find(l => l.country.toLowerCase() === lowerName) 49 if (leader) { 50 ["government", "state"].forEach(type => { 51 if (!leader[type]) return 52 let lastName = leader[type].split(" ").pop(); 53 c.terms.push(lastName.toLowerCase()); 54 }) 55 } 56 c.data = countryData.find(d => parseInt(d.ccn3) === parseInt(c.id)); 57 c.terms = c.terms.concat(c.data.capital.map(cap => cap.toLowerCase())); 58 const demonym = c.data.demonym.toLowerCase(); 59 c.terms.push(demonym); 60 c.terms.push(demonym + "s"); 61 c.terms.push(c.data.name.common.toLowerCase()); 62 c.terms = c.terms.map(t => getTokens([t])); 63 }); 64} 65 66function loadStories() { 67 $('.loading').show(); 68 initializeCountryData(); 69 let parser = new RSSParser(); 70 let prom = Promise.resolve(); 71 let total = feeds.length; 72 let done = 0; 73 feeds.forEach(name => {
74 let cached = localStorage.getItem(name); 75 if (cached) { 76 cached = JSON.parse(cached); 77 if (new Date(cached.retrieved).getTime() + MAX_AGE >= new Date().getTime()) { 78 console.log('using cached feed for', name); 79 addStories(name, cached); 80 return 81 } else { 82 console.log('cache is stale for', name); 83 } 84 } 85 prom = prom.then(_ => parser.parseURL(getFeed(name)).then(feed => { 86 feed.retrieved = new Date(); 87 localStorage.setItem(name, JSON.stringify(feed)); 88 addStories(name, feed); 89 })); 90 }) 91 prom = prom.then(_ => { 92 for (let country in storiesByCountry) { 93 let stories = storiesByCountry[country]; 94 stories.sort((s1, s2) => { 95 return new Date(s2.pubDate).getTime() - new Date(s1.pubDate).getTime(); 96 }) 97 } 98 $('.loading').hide(); 99 }); 100 return prom; 101} 102 103function addStories(name, feed) { 104 let noCountry = 0; 105 feed.items.forEach(item => { 106 let country = null; 107 if (name === 'britain') { 108 country = countryList.find(c => c.name === 'United Kingdom'); 109 } else if (name === 'united-states') { 110 country = countryList.find(c => c.name === 'United States'); 111 } else if (name === 'china') { 112 country = countryList.find(c => c.name === 'China'); 113 } else { 114 const tokens = getTokens([item.title, item.contentSnippet]); 115 country = countryList.find(c => matchTermList(tokens, c.terms)); 116 } 117 if (country) { 118 storiesByCountry[country.id] = storiesByCountry[country.id] || []; 119 storiesByCountry[country.id].push(item); 120 } else { 121 console.log("no country match:", item.title); 122 noCountry++; 123 } 124 }) 125 console.log(name, noCountry, "items with no country out of", feed.items.length); 126} 127 128function getTokens(strs) { 129 let tokens = []; 130 strs.forEach(str => { 131 tokens = tokens.concat(str.toLowerCase().split(' ')); 132 }); 133 tokens = tokens.map(t => { 134 t = t.replace(/'s$/, ''); 135 t = t.replace(/âs$/, ''); 136 t = t.replace(/\W+$/, ''); 137 return t 138 }) 139 return tokens 140} 141 142function matchTermList(tokens, termList) { 143 for (let term of termList) { 144 if (matchTokens(tokens, term)) return true 145 } 146 return false 147} 148 149function matchTokens(tokensToSearch, tokens) { 150 for (let i = 0; i <= tokensToSearch.length - tokens.length; ++i) { 151 let match = true; 152 for (let j = 0; j < tokens.length; ++j) { 153 if (tokens[j] !== tokensToSearch[i + j]) { 154 match = false; 155 break; 156 } 157 } 158 if (match) return true; 159 } 160 return false; 161}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.