Initial readings data on gemini capsule.

This commit is contained in:
David Soulayrol 2023-03-26 12:46:31 +02:00
commit f49202db85
12 changed files with 2004 additions and 138 deletions

View file

@ -0,0 +1,134 @@
const debug = require('debug')('metalsmith-gemini:comments')
const moment = require('moment')
module.exports = plugins
/**
* Metalsmith plugins collection to handle comments.
*
* @return {Function}
*/
function plugins (helpers) {
function store (collection, key, value) {
if (collection[key] === undefined) { collection[key] = [] }
collection[key].push(value)
}
function sortedComments (metalsmith, index) {
const sortedComments = {}
metalsmith.metadata().comments.forEach(entry => {
if (index instanceof Function) {
store(sortedComments, index(entry.comment), entry)
} else {
store(sortedComments, entry.comment[index], entry)
}
})
return sortedComments
}
function createCommentsIndex (index, entries) {
let body = '# ' + index + '\n\n'
entries.forEach(entry => {
body += '=> /' + entry.filename + ' ' + entry.book.title + ', ' + entry.book.author
if (index === entry.comment.medium) {
body += ' (' + entry.comment.date.year() + ')\n'
} else {
body += ' (sur ' + entry.comment.medium + ')\n'
body += entry.comment.excerpt
}
})
body += '\n=> /chroniques Retour à la liste complète\n'
return body
}
return {
buildMetadata: function (files, metalsmith, done) {
const EXCERPT_MAX_LENGTH = 300
const comments = []
// TODO: The excerpt should take care of words boundaries
const buildExcerpt = function (contents) {
let length = contents.indexOf('\n')
if (length === -1) {
length = contents.length
}
let excerpt = contents.substr(0, Math.min(length, EXCERPT_MAX_LENGTH))
if (length > EXCERPT_MAX_LENGTH) {
excerpt += '…'
}
return excerpt + '\n'
}
Object.keys(files).forEach(filename => {
if (helpers.isGemText(filename)) {
const data = files[filename]
if (data.comment !== undefined) {
data.comment.date = moment(data.comment.date)
data.comment.excerpt = buildExcerpt(data.contents.toString())
data.comment.size = data.contents.length
data.layout = 'gemini-comment.njk'
comments.push({
book: data.book,
comment: data.comment,
filename
})
}
}
})
debug('Identified %d comments', comments.length)
/* Comment are ordered by title by default. */
comments.sort((a, b) => a.book.title.localeCompare(b.book.title))
metalsmith.metadata().comments = comments
done()
},
rootIndex: function (files, metalsmith, done) {
let body = '# Chroniques\n'
body += '\n## Par support\n\n'
Object.keys(sortedComments(metalsmith, 'medium')).forEach((medium) => {
body += '=> ' + helpers.filepath(medium, ['/chroniques']) + ' ' + medium + '\n'
})
body += '\n## Par année\n\n'
Object.keys(sortedComments(metalsmith, (c) => c.date.year())).forEach((year) => {
body += '=> /chroniques/' + year + ' ' + year + '\n'
})
helpers.createFile(files, 'chroniques/index.gemini', body)
done()
},
mediaIndexes: function (files, metalsmith, done) {
Object.entries(sortedComments(metalsmith, 'medium')).forEach(([medium, comments]) => {
helpers.createFile(files, helpers.filepath(medium, ['chroniques']), createCommentsIndex(medium, comments))
})
done()
},
yearIndexes: function (files, metalsmith, done) {
Object.entries(sortedComments(metalsmith, (c) => c.date.year())).forEach(([year, comments]) => {
helpers.createFile(files, helpers.filepath('index', ['chroniques', year]), createCommentsIndex(year, comments))
})
done()
}
}
}

View file

@ -0,0 +1,246 @@
const debug = require('debug')('metalsmith-gemini:readings')
const path = require('path')
module.exports = plugins
/**
* Metalsmith plugins collection to handle readings.
*
* @return {Function}
*/
function plugins (helpers) {
const Categories = Object.freeze({
0: 'Fictions',
10: 'Essais',
20: 'BD',
30: 'Poésie',
40: 'Théâtre'
})
function getBibAuthors (book) {
// TODO: books should have list of authors
const authors = book.a
return authors
}
function getBibPublishing (book, publishers) {
const p = publishers[book.p]
let published
if (p != null) {
published = p.c + ' : ' + p.n
if (book.c != null) {
published += ' (' + p.C[book.c] + ')'
}
published += ', ' + (book.y || '?')
} else {
published = '?'
if (p !== undefined) {
console.warn('Unknown publisher "' + book.p + '"')
}
}
return published
}
function getBibTitle (book) {
let title = book.t
if (!'.!?'.includes(title.charAt(title.length - 1))) {
title += '.'
}
if (book.v != null) {
title += ' ' + book.v + ' vols.'
}
return title
}
function writeLine (book, publishers) {
return '* ' + getBibAuthors(book) + '. ' + getBibTitle(book) + ' ' + getBibPublishing(book, publishers) + '\n'
}
function createIndex (year, books, publishers) {
const sections = {}
let body = '# ' + year + '\n\n'
Object.keys(Categories).forEach((key) => { sections[key] = '' })
books.forEach(b => {
const line = writeLine(b, publishers)
if (line.length > 0) {
sections[b.C || 0] += line
}
})
Object.entries(Categories).forEach(([key, title]) => {
const section = sections[key]
if (section.length !== 0) {
body += '## ' + title + '\n\n'
body += section + '\n'
}
})
return body
}
function getBy (books, key) {
const map = {}
books.forEach(book => {
if (map[book[key]] == null) {
map[book[key]] = []
}
map[book[key]].push(book)
})
return map
}
function getByPublisher (books) {
return getBy(books, 'p')
}
function getByYear (books) {
return getBy(books, 'Y')
}
function isJson (filename) {
return /\.json$/.test(path.extname(filename))
}
return {
buildMetadata: function (files, metalsmith, done) {
const books = []
let publishers = {}
Object.keys(files).forEach(filename => {
if (isJson(filename)) {
if (filename.endsWith('publishers.json')) {
publishers = JSON.parse(files[filename].contents)
debug('Identified %d publishers', Object.keys(publishers).length)
} else {
const year = path.basename(filename, '.json')
JSON.parse(files[filename].contents).forEach(b => {
b.Y = year
books.push(b)
})
}
delete files[filename]
}
})
debug('Identified %d books', books.length)
metalsmith.metadata().books = books
metalsmith.metadata().publishers = publishers
done()
},
authorsIndex: function (files, metalsmith, done) {
const authors = {}
const publishers = metalsmith.metadata().publishers
let body = '# Auteurs\n'
metalsmith.metadata().books.forEach(book => {
if (authors[book.a] == null) {
authors[book.a] = []
}
authors[book.a].push(book)
})
Object.keys(authors).sort().forEach(author => {
const lines = []
authors[author].sort().forEach(book => {
lines.push('* ' + getBibTitle(book) + ' ' + getBibPublishing(book, publishers) + '\n')
})
body += '\n## ' + author + '\n' + lines.sort().join('')
})
helpers.createFile(files, helpers.filepath('auteurs', ['lectures']), body)
done()
},
publishersIndex: function (files, metalsmith, done) {
const publishers = metalsmith.metadata().publishers
let body = '# Éditions\n'
Object.entries(getByPublisher(metalsmith.metadata().books)).forEach(([pub, books]) => {
const p = publishers[pub]
// TODO: Extract collections in a set
if (p != null) {
// let collections = {}
const lines = []
books.forEach(book => {
lines.push('* ' + getBibAuthors(book) + '. ' + getBibTitle(book) + '\n')
})
body += '\n## ' + p.n + '\n' + lines.sort().join('')
}
})
helpers.createFile(files, helpers.filepath('editeurs', ['lectures']), body)
done()
},
rootIndex: function (files, metalsmith, done) {
let body = '# Lectures\n\n'
body += '=> auteurs.gemini La liste alphabétique des auteurs lus\n'
body += '=> editeurs.gemini La liste alphabétique des éditeurs rencontrés\n'
body += '=> stats.gemini Quelques statistiques\n\n'
body += '## Par année\n\n'
Object.keys(getByYear(metalsmith.metadata().books)).forEach(year => {
body += '=> /lectures/' + year + '.gemini ' + year + '\n'
})
helpers.createFile(files, helpers.filepath('index', ['lectures']), body)
done()
},
statisticsIndex: function (files, metalsmith, done) {
const stats = { a: new Set(), y: {} }
const books = metalsmith.metadata().books
let body = '# Quelques statistiques\n\n'
books.forEach(book => {
stats.a.add(book.a)
if (stats.y[book.Y] == null) {
stats.y[book.Y] = []
}
stats.y[book.Y].push(book)
})
body += 'Depuis le printemps 2010, ce sont ' + books.length + ' lectures recensées.\n'
body += 'Ces lectures couvrent ' + stats.a.size + ' auteurs différents.\n'
body += '## Par année\n'
Object.entries(stats.y).forEach(([year, books]) => {
body += '* ' + year + ': ' + books.length + ' livres\n'
})
helpers.createFile(files, helpers.filepath('stats', ['lectures']), body)
done()
},
yearIndexes: function (files, metalsmith, done) {
const publishers = metalsmith.metadata().publishers
Object.entries(getByYear(metalsmith.metadata().books)).forEach(([year, books]) => {
// Sort by category and then by authors
const content = createIndex(year, books, publishers)
helpers.createFile(files, helpers.filepath(year, ['lectures']), content)
})
done()
}
}
}

View file

@ -1,9 +1,10 @@
const comments = require('./metalsmith-gemini-comments.js')
const config = require('./config.js')
const debug = require('debug')('gemini')
const Metalsmith = require('metalsmith')
const layouts = require('@metalsmith/layouts')
const moment = require('moment')
const path = require('path')
const readings = require('./metalsmith-gemini-readings.js')
const slug = require('slug')
const statistics = require('./metalsmith-statistics-plugin')
@ -11,144 +12,33 @@ const __PROD__ = process.env.NODE_ENV === 'production'
moment.locale('fr')
function store (collection, key, value) {
if (collection[key] === undefined) { collection[key] = [] }
collection[key].push(value)
}
function isGemText (filename) {
return /\.gmi$|\.gemini$/.test(path.extname(filename))
}
function filepath (filename, segments) {
const name = slug(filename, { mode: 'rfc3986' }) + '.gemini'
return path.join.apply(null, (segments || ['./']).concat([name]))
}
function sortedComments (metalsmith, index) {
const sortedComments = {}
const helpers = {
createFile: function (files, filename, body) {
const entry = Object.assign({
contents: Buffer.from(body)
})
metalsmith.metadata().comments.forEach(entry => {
if (index instanceof Function) {
store(sortedComments, index(entry.comment), entry)
} else {
store(sortedComments, entry.comment[index], entry)
}
})
files[filename] = entry
},
return sortedComments
}
filepath: function (filename, segments) {
const name = slug(filename, { mode: 'rfc3986' }) + '.gemini'
return path.join.apply(null, (segments || ['./']).concat([name]))
},
function createIndex (files, filename, body) {
const entry = Object.assign({
contents: Buffer.from(body)
})
files[filename] = entry
}
function createCommentsIndex (index, entries) {
let body = '# ' + index + '\n\n'
entries.forEach(entry => {
body += '=> /' + entry.filename + ' ' + entry.book.title + ', ' + entry.book.author
if (index === entry.comment.medium) {
body += ' (' + entry.comment.date.year() + ')\n'
} else {
body += ' (sur ' + entry.comment.medium + ')\n'
body += entry.comment.excerpt
}
})
body += '\n=> /chroniques Retour à la liste complète\n'
return body
}
function rootIndexPlugin (files, metalsmith, done) {
let body = '# Chroniques\n'
body += '\n## Par support\n\n'
Object.keys(sortedComments(metalsmith, 'medium')).forEach((medium) => {
body += '=> ' + filepath(medium, ['/chroniques']) + ' ' + medium + '\n'
})
body += '\n## Par année\n\n'
Object.keys(sortedComments(metalsmith, (c) => c.date.year())).forEach((year) => {
body += '=> /chroniques/' + year + ' ' + year + '\n'
})
createIndex(files, 'chroniques/index.gemini', body)
done()
}
function mediaIndexPlugin (files, metalsmith, done) {
Object.entries(sortedComments(metalsmith, 'medium')).forEach(([medium, comments]) => {
createIndex(files, filepath(medium, ['chroniques']), createCommentsIndex(medium, comments))
})
done()
}
function yearsIndexPlugin (files, metalsmith, done) {
Object.entries(sortedComments(metalsmith, (c) => c.date.year())).forEach(([year, comments]) => {
createIndex(files, filepath('index', ['chroniques', year]), createCommentsIndex(year, comments))
})
done()
}
function buildCommentsMetadata (files, metalsmith, done) {
const EXCERPT_MAX_LENGTH = 300
const comments = []
// TODO: The excerpt should take care of words boundaries
const buildExcerpt = function (contents) {
let length = contents.indexOf('\n')
if (length === -1) {
length = contents.length
}
let excerpt = contents.substr(0, Math.min(length, EXCERPT_MAX_LENGTH))
if (length > EXCERPT_MAX_LENGTH) {
excerpt += '…'
}
return excerpt + '\n'
isGemText: function (filename) {
return /\.gmi$|\.gemini$/.test(path.extname(filename))
}
Object.keys(files).forEach(filename => {
if (isGemText(filename)) {
const data = files[filename]
if (data.comment !== undefined) {
data.comment.date = moment(data.comment.date)
data.comment.excerpt = buildExcerpt(data.contents.toString())
data.comment.size = data.contents.length
data.layout = 'gemini-comment.njk'
comments.push({
book: data.book,
comment: data.comment,
filename
})
}
}
})
debug('Identified %d comments: %s', comments.length)
/* Comment are ordered by title by default. */
comments.sort((a, b) => a.book.title.localeCompare(b.book.title))
metalsmith.metadata().comments = comments
done()
}
const commentsGenerator = comments(helpers)
const readingsGenerator = readings(helpers)
module.exports = new Metalsmith(config.paths.projectRoot)
.clean(__PROD__)
.metadata({
@ -156,11 +46,17 @@ module.exports = new Metalsmith(config.paths.projectRoot)
})
.source(config.paths.geminiSource)
.destination(config.paths.geminiDestination)
.use(buildCommentsMetadata)
.use(rootIndexPlugin)
.use(mediaIndexPlugin)
.use(yearsIndexPlugin)
.use(commentsGenerator.buildMetadata)
.use(commentsGenerator.rootIndex)
.use(commentsGenerator.mediaIndexes)
.use(commentsGenerator.yearIndexes)
// TODO: atom.xml (et lien depuis l'index)
.use(readingsGenerator.buildMetadata)
.use(readingsGenerator.authorsIndex)
.use(readingsGenerator.publishersIndex)
.use(readingsGenerator.rootIndex)
.use(readingsGenerator.statisticsIndex)
.use(readingsGenerator.yearIndexes)
.use(layouts({
default: 'gemini.njk',
pattern: '**/*.gemini',