PageSourceSearch

https://rbren.io/worldnews/stories.js

js rbren.io collected 2026-10-03 23:21:16 UTC 4,671 bytes, 161 lines download raw bytes

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.