@@ -1,154 +1,162 @@
11'use strict'
22
3−function Wiki (sites) {
4− const feeds = {}
5− this.sites = sites
6− this._el = document.createElement('div')
7− this._el.id = 'wiki'
8− this._sidebar = document.createElement('div')
9− this._sidebar.id = 'sidebar'
10− this._categories = document.createElement('ul')
11− this._categories.id = 'categories'
12− this._entry = document.createElement('div')
13− this._entry.id = 'entry'
14−
15− this._footer = document.createElement('p')
16− this._footer.id = 'footer'
17− this._footer.innerHTML = `The <strong>Wiki</strong> is a decentralized encyclopedia, to join the conversation, add a <a href="https://github.com/XXIIVV/webring#joining-the-wiki">wiki:</a> field to your entry in the <a href="https://github.com/XXIIVV/Webring/">webring</a>.`
18−
19− this.loc = ''
20− this.byName = {}
21− this.byCat = {}
22− this.byAuthor = {}
23−
24− this.install = (host) => {
25− this._el.appendChild(this._entry)
26− this._sidebar.appendChild(this._categories)
27− this._el.appendChild(this._sidebar)
28− this._el.appendChild(this._footer)
29− host.appendChild(this._el)
30− this.fetch()
3+const Wiki = sites => {
4+ const entries = {}
5+ const main = document.getElementById('main')
6+ const aside = document.getElementById('aside')
7+ const progress = document.getElementById('progress')
8+ const progressCounter = Progress(sites.map(site => site.wiki))
9+ const authors = new Set()
10+ const terms = new Set()
11+
12+ const storeEntry = (author, url, category, term, entry) => {
13+ if (!(category in entries)) {
14+ entries[category] = {}
15+ entries[category][term] = [{ entry, authors: [{ author, url }] }]
16+ } else if (!(term in entries[category])) {
17+ entries[category][term] = [{ entry, authors: [{ author, url }] }]
18+ } else {
19+ const possibleIndex = entries[category][term].findIndex(existing => existing.entry === entry)
20+ possibleIndex > -1
21+ ? entries[category][term][possibleIndex].authors.push({ author, url })
22+ : entries[category][term].push({ entry, authors: [{ author, url }] })
23+ }
24+ terms.add(term)
25+ authors.add(author)
3126 }
3227
33− this.start = () => {
34− this._entry.innerHTML = 'Loading..'
28+ const storeEntries = (author, url, category, entries) => {
29+ Array.isArray(entries)
30+ ? storeEntry(author, url, category, 'list-definitions', entries)
31+ : Object.keys(entries).forEach(entry => storeEntry(author, url, category, entry, entries[entry]))
3532 }
3633
37− this.refresh = () => {
38− // Main
39− if (this.loc) {
40− if (this.byCat[this.loc]) {
41− const formatHtml = (entries, entry) => `${entries} ${this.templateTerm(entry.name, this.byName[entry.name])}<br />`
42− const html = this.byCat[this.loc].reduce(formatHtml, '')
43− this._entry.innerHTML = html
44− } else if (this.byName[this.loc]) {
45− const html = `${this.templateTerm(this.loc, this.byName[this.loc])}<br />${this.templateRelated(this.loc)}`
46− this._entry.innerHTML = html
47− } else {
48− const html = `Unknown: ${this.loc}. Retun <a href='#'>home</a>, or try a <a href='${this.random()}'>random page</a>.`
49− this._entry.innerHTML = html
50− }
51− } else {
52− this._entry.innerHTML = `Click a /topic to get started, or try a <a href='#${this.random()}'>random page</a>.<br />The wiki contains ${Object.keys(this.byName).length} terms, in ${Object.keys(this.byCat).length} categories, by ${Object.keys(this.byAuthor).length} authors.`
53− }
54− // Sidebar
55− this._categories.innerHTML = Object.keys(this.byCat).reduce((acc, id) =>
56− { return this.byCat[id].length > 5
57− ? `${acc}<li ${wiki.at(id) ? 'class="selected"' : ''}>
58− <a href='#${id}' data-msgs='${this.byCat[id].length}'>${id}</a>
59− </li>`
60− : acc
61− } , `<li ${wiki.at() ? 'class="selected"' : ''}>
62− <a href='#' data-msgs='${Object.keys(this.byName).length}'>home</a>
63− </li>`
64− )
34+ const transform = (author, url, ndtl) => {
35+ Object.keys(ndtl).forEach(category => storeEntries(author, url, category, ndtl[category]))
6536 }
6637
67− this.go = () => {
68− this.loc = decodeURIComponent( window.location.hash.substr(1) ).toUpperCase()
69− this.refresh()
38+ const parse = (site, content) => {
39+ console.log('Wiki', 'Parsing ' + site.wiki)
40+ const ndtl = indental(content)
41+ transform(site.author, site.url, ndtl)
7042 }
7143
72− this.at = (q = '') => {
73− return this.loc.toUpperCase() === q.toUpperCase() || (this.cat(this.loc).toUpperCase() === q.toUpperCase() && q !== '')
74− }
44+ const selected = (key, category) => key.toLowerCase() === category.toLowerCase()
45+ ? 'selected'
46+ : ''
7547
76− this.random = () => {
77− const keys = Object.keys(this.byCat)
48+ const randomTerm = () => {
49+ const keys = Object.keys(entries)
7850 return keys[Math.floor(Math.random() * keys.length)]
7951 }
8052
81− this.related = (name) => {
82− return this.byCat[this.cat(name)]
53+ const formatSideBarCat = (key, entries) => (currentHtml, cat) => {
54+ const catLength = Object.keys(entries[cat]).length
55+ const newHtml = `<li class='${selected(key, cat)}'}'>
56+ <a href='#${cat}' data-msgs='${catLength}'>${cat.toLowerCase()}</a>
57+ </li>`
58+ return `${currentHtml}${newHtml}`
8359 }
8460
85− this.cat = (name) => {
86− return this.byName[name] ? this.byName[name][0].cat : ''
61+ const formatLinks = text => text.replace(
62+ /\b((https?|dat):\/\/[^"\s]+)/g,
63+ '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>'
64+ )
65+
66+ const formatAuthor = (authors, author) => {
67+ return `${authors}<a class='author' target='_blank' href='${author.url}'> @${author.author}</a>`
8768 }
8869
89− this.templateRelated = (name) => {
90− const relatedWords = this.related(name)
91− const html = `<ul class='col3'>${relatedWords.reduce( (acc, item, id) =>
92− { return `${acc}<li ${wiki.at(item.name) ? 'class="selected"' : ''}>
93− <a href='#${item.name.toLowerCase()}'>${item.name.toLowerCase()}</a>
70+ const formatEntry = (definitions, definition) => {
71+ const formatEntryList = entryList => entryList.reduce((items, item) => `${items}<li>${formatLinks(item)}</li>`, '')
72+
73+ const newHtml = typeof definition.entry === 'string'
74+ ? `<li>${formatLinks(definition.entry)}
75+ - ${definition.authors.reduce(formatAuthor, '')}
9476 </li>`
95− }, '') }
96− </ul>`
97− return html
98− }
77+ : `<li><ul>${formatEntryList(definition.entry)}
78+ - ${definition.authors.reduce(formatAuthor, '')}
79+ </ul></li>`
9980
100− this.templateTerm = (name, entries) => {
101− const formatEntry = (acc, entry) => {
102− if (typeof entry.value === 'string') {
103− return `${acc}<li>${entry.value}<a class='author' target='_blank' href='${entry.origin.url}'> — @${entry.origin.author}</a></li>`
104− } else {
105− const listItems = entry.value.reduce((items, item) => `${items}<li>${item}</li>`, '')
106− return `${acc}<li><ul>${listItems}<a class='author' target='_blank' href='${entry.origin.url}'> — @${entry.origin.author}</a></ul></li>`
107− }
108− }
81+ return `${definitions}${newHtml}`
82+ }
10983
110− const htmlEntries = entries.reduce(formatEntry, '')
111− return `<ul class="term"><li class="name"><strong>${name.toLowerCase()}</strong></li>${htmlEntries}</ul>`
84+ const formatEntries = entries => (currentHtml, category) => {
85+ const definitions = entries[category].reduce(formatEntry, '')
86+ const title = category === 'list-definitions'
87+ ? ''
88+ : `<li class='name'><strong>${category.toLowerCase()}</strong></li>`
89+ return `${currentHtml}<ul class='term'>${title}${definitions}</ul>`
11290 }
11391
114− this.fetch = () => {
115− console.log('Wiki', 'Fetching..')
116− for (const site of sites) {
117− if (!site.wiki || !site.author) { continue }
118− fetch(site.wiki, { cache: 'no-store' }).then(x => x.text()).then((content) => {
119− this.parse(site, content)
120− this.refresh(feeds)
121− }).catch((err) => {
122− console.warn(`${site.wiki}`, err)
123− })
92+ const findRelated = key => {
93+ const searchOtherCategories = entry => {
94+ return Object.keys(entries)
95+ .filter(category => Object.keys(entries[category]).some(otherCat => otherCat === entry))
12496 }
97+
98+ const foundMatches = Object.keys(entries[key])
99+ .filter(entry => entry !== 'list-definitions')
100+ .map(searchOtherCategories)
101+ const flattened = [].concat.apply([], foundMatches)
102+ return Array.from(new Set(flattened)).filter(related => related !== key)
125103 }
126104
127− this.add = (name, value, cat, origin) => {
128− this.byName[name] = this.byName[name] || []
129− this.byName[name].push({ name, value, cat, origin })
105+ const formatRelated = (allRelated, relatedCategory) => {
106+ const newHtml = `<span><a href='#${relatedCategory}'>/${relatedCategory.toLowerCase()}</a> </span>`
107+ return `${allRelated}${newHtml}`
108+ }
130109
131− this.byCat[cat] = this.byCat[cat] || []
132− this.byCat[cat].push({ name, value, cat, origin })
110+ const refresh = key => {
111+ if (key) {
112+ if (entries[key] === undefined) return
113+ const items = Object.keys(entries[key]).sort().reduce(formatEntries(entries[key]), '')
114+ const relatedCategories = findRelated(key)
115+ const relatedHtml = relatedCategories.length > 0
116+ ? relatedCategories.reduce(formatRelated, 'Related categories: ')
117+ : ''
118+ main.innerHTML = `<div class='related'><h1>${key}</h1>${relatedHtml}</div>${items}`
119+ } else {
120+ const html = `Click a /topic to get started, or try a <a href='#${randomTerm()}'>random page</a>`
121+ const stats = `The wiki contains ${terms.size} terms in ${Object.keys(entries).length} categories, from ${authors.size} authors.`
122+ main.innerHTML = `<h1>WIKI</h1>${html}<br />${stats}`
123+ }
133124
134− this.byAuthor[origin.author] = this.byAuthor[origin.author] || []
135− this.byAuthor[origin.author].push({ name, value, cat, origin })
125+ const homeCat = `<li class='${selected(key, '')}'>
126+ <a href='#' data-msgs='${terms.size}'>home</a>
127+ </li>`
128+ const cats = Object.keys(entries).sort().reduce(formatSideBarCat(key, entries), homeCat)
129+ aside.innerHTML = `<ul>${cats}</ul>`
136130 }
137131
138− this.parse = (site, content) => {
139− console.log('Wiki', 'Parsing ' + site.wiki)
140− const cats = indental(content)
141− for (const cat in cats) {
142− const terms = cats[cat]
143− for (const name in terms) {
144− this.add(name, terms[name], cat, site)
145− }
146− }
132+ const refreshProgress = () => {
133+ progress.innerHTML = `${progressCounter}`
147134 }
148135
149− String.prototype.toUrl = function () { return this.toLowerCase().replace(/ /g, '+').replace(/[^0-9a-z+:\-./~]/gi, '').trim() }
136+ return {
137+ initialize: hash => {
138+ console.log('Wiki', 'Fetching...')
139+ refreshProgress()
140+ sites.filter(site => site.wiki && site.author).forEach(site => {
141+ fetch(site.wiki, { cache: 'no-store' }).then(x => x.text()).then(content => {
142+ parse(site, content)
143+ refresh(stripHash(hash))
144+ progressCounter.success(site.wiki).then(refreshProgress)
145+ }).catch((err) => {
146+ console.warn(`${site.wiki}`, err)
147+ progressCounter.failure(site.wiki).then(refreshProgress)
148+ })
149+ })
150+ },
151+ refresh: key => { refresh(key) }
152+ }
153+}
150154
151− window.onload = this.go
152− window.onhashchange = this.go
155+const stripHash = hash => {
156+ const decoded = decodeURIComponent(hash)
157+ return decoded.charAt(0) === '#' ? decoded.substring(1) : decoded
153158}
154159
160+window.addEventListener('hashchange', () => {
161+ wiki.refresh(stripHash(window.location.hash).toUpperCase())
162+})