web/test_lib.js
// https://ourbigbook/ourbigbook/demo-data
//
// Need a separate file from test.js because Mocha automatically defines stuff like it,
// which would break non-Mocha requirers.
const fs = require('fs')
const path = require('path')
const perf_hooks = require('perf_hooks')
const lodash = require('lodash')
const ourbigbook = require('ourbigbook')
const back_js = require('./back/js')
const convert = require('./convert')
const models = require('./models')
const { performance } = require('perf_hooks')
const now = performance.now.bind(performance)
let printTimeNow;
function printTime() {
const newNow = now()
console.error((newNow - printTimeNow)/1000.0)
printTimeNow = newNow
}
// https://stackoverflow.com/questions/563406/add-days-to-javascript-date
function addDays(oldDate, days) {
const newDate = new Date(oldDate.valueOf());
newDate.setDate(oldDate.getDate() + days);
return newDate;
}
const date0 = new Date(2000, 0, 0, 0, 0, 0, 0)
const userData = [
['Barack Obama', 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Official_portrait_of_Barack_Obama.jpg/160px-Official_portrait_of_Barack_Obama.jpg'],
['Donald Trump', 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Donald_Trump_official_portrait.jpg/160px-Donald_Trump_official_portrait.jpg'],
['Xi Jinping', 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/32/Xi_Jinping_2019.jpg/90px-Xi_Jinping_2019.jpg'],
['Mao Zedong', 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e8/Mao_Zedong_portrait.jpg/90px-Mao_Zedong_portrait.jpg'],
['Isaac Newton', 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3b/Portrait_of_Sir_Isaac_Newton%2C_1689.jpg/220px-Portrait_of_Sir_Isaac_Newton%2C_1689.jpg'],
['Joe Biden', 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/68/Joe_Biden_presidential_portrait.jpg/160px-Joe_Biden_presidential_portrait.jpg'],
['Li Hongzhi', 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Li_Hongzhi_1.jpg/200px-Li_Hongzhi_1.jpg'],
['Jiang Zemin', 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/ce/Jiang_Zemin_St._Petersburg.jpg/90px-Jiang_Zemin_St._Petersburg.jpg'],
['John F. Kennedy', 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/John_F._Kennedy%2C_White_House_color_photo_portrait.jpg/160px-John_F._Kennedy%2C_White_House_color_photo_portrait.jpg'],
['Erwin Schrödinger', 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Erwin_Schrodinger2.jpg/170px-Erwin_Schrodinger2.jpg'],
['Jesus', 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Spas_vsederzhitel_sinay.jpg/220px-Spas_vsederzhitel_sinay.jpg'],
['Deng Xiaoping', 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/Deng_Xiaoping_and_Jimmy_Carter_at_the_arrival_ceremony_for_the_Vice_Premier_of_China._-_NARA_-_183157-restored%28cropped%29.jpg/220px-Deng_Xiaoping_and_Jimmy_Carter_at_the_arrival_ceremony_for_the_Vice_Premier_of_China._-_NARA_-_183157-restored%28cropped%29.jpg'],
['George W. Bush', 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/George-W-Bush.jpeg/160px-George-W-Bush.jpeg'],
['Einstein', 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Einstein_patentoffice.jpg/170px-Einstein_patentoffice.jpg'],
['Bill Clinton', 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Bill_Clinton.jpg/160px-Bill_Clinton.jpg'],
['Gautama Buddha', 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Mahapajapati.jpg/220px-Mahapajapati.jpg'],
['President Reagan', 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/Official_Portrait_of_President_Reagan_1981.jpg/165px-Official_Portrait_of_President_Reagan_1981.jpg'],
['Euclid of Alexandria', 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/ce/Scuola_di_atene_23.jpg/220px-Scuola_di_atene_23.jpg'],
['Richard Nixon', 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Richard_Nixon_presidential_portrait_%281%29.jpg/160px-Richard_Nixon_presidential_portrait_%281%29.jpg'],
['Moses', 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Guido_Reni_-_Moses_with_the_Tables_of_the_Law_-_WGA19289.jpg/220px-Guido_Reni_-_Moses_with_the_Tables_of_the_Law_-_WGA19289.jpg'],
]
const articleData = [
['Mathematics', [
['Algebra', [
['Linear algebra', [
['Vector space', []],
]],
['Abstract algebra', []],
]],
['Calculus', [
['Derivative', []],
['Integral', [
['Fundamental theorem of calculus', [
['Proof of the fundamental theorem of calculus', []],
]],
['$L^p$ space', []],
]],
]],
]],
['Natural science', [
['Physics', [
['Quantum mechanics', [
['Schrödinger equation', [
['Schrödinger equation solution for the hydrogen atom', [
['Atomic orbital', [
['Principal quantum number', []],
['Azimuthal quantum number', []],
['Magnetic quantum number', []],
]],
], { headerArgs: '{c}' }],
], { headerArgs: '{c}' }],
['Quantum mechanics experiment', [
['Emission spectrum', [
['Rydberg formula', [], { headerArgs: '{c}' }],
['Fine structure', [
['Hyperfine structure', []],
]],
]],
['Double-slit experiment', []],
]],
]],
['Special relativity', [
['Lorentz transformation', [
['Time dilation', []],
], { headerArgs: '{c}' }],
]],
], { toplevel: true }],
['Chemistry', [
['Chemical element', [
['Hydrogen', [
['Water', []],
]],
['Helium', []],
['Carbon', [
['Carbon-14', []],
]],
]],
['Organic chemistry', [
]],
], { toplevel: true }],
['Biology', [
['Molecular biology', [
['DNA', [], { headerArgs: '{c}' }],
['Protein', []],
]],
['Cell biology', [
['Organelle', [
['Mitochondrion', []],
['Ribosome', [
['Ribosome large subunit', []],
['Ribosome small subunit', []],
]],
]],
]],
], { toplevel: true }],
]],
['Test data',
[
['Test scope', [
['Test scope 1', [
['Test scope 1 1', []],
['Test scope 1 2', []],
]],
['Test scope 2', []],
], { headerArgs: '{scope}' }],
['Test tag', [
['Test tagged', [], { headerArgs: '{tag=Test tagger}' }],
['Test tagger', []],
]],
['Test wiki', [], { headerArgs: '{wiki}' }],
['Test child', [
['Test child 1', [
['Test child 1 1', []],
], { body: `Link to outsider: <test child 2>
Link to parent: <test child>
Link to child: <test child 1 1>
Link to external: http://example.com
Link to topic: <#mathematics>
` }],
['Test child 2', [], { body: `Link to synonym: <Test child with synonym 2>
` }],
['Test child with synonym', [], {
body: `= Test child with synonym 2
{synonym}
= Test child with synonym 3
{synonym}
`
}],
]],
],
{
body: `Block math: <equation My favorite equation>
$$
\\frac{1}{\\sqrt{2}}
$$
{title=My favorite equation}
And another: <equation My second favorite equation>
$$
\\frac{1}{\\sqrt{2}}
$$
{title=My second favorite equation}
Ourbigbook defined LaTeX macro: $\\abs{x}$
`
}
],
]
const issueData = [
['Test issue', `Link to article: <test data>
Link to ID in this issue: <test issue 2>
== Test issue 2
=== Test issue 3
== Test child
Conflict resolution between issue IDs and article IDs:
* to issue: <test child>
* to article: </test child>
`],
['There\'s a typo in this article at "mathmatcs"', ``],
['Add mention of the fundamental theorem of calculus', `The fundamental theorem of calculus is very important to understanding this subject.
I would add something like:
\\Q[
The reason why the superconductor laser is blue, is due to the integral of its resonance modes.
From the fundamental theorem of calculus, we understand that this is because the derivative of the temperature is too small.
]
== Also mentions Newton's rule
As an added bonus, a mention of Newton's rule would also be very useful.
`],
['$\\sqrt{1 + 1} = 3$, not 2 as mentioned', 'I can\'t believe you got such a basic fact wrong!'],
['The code `f(x) + 1` should be `f(x) + 2`', 'Zero indexing always gets me too.'],
]
const commentData = [
`= My test comment
Link to ID in comment: <My test comment 2>
Link to article: <test data>
== My test comment 2
=== My test comment 3
== Test child
Conflict resolution between issue IDs and article IDs:
* to issue: <test child>
* to article: </test child>
`,
// `My test comment without a header
//
//Link to article: <test data>
//`,
'Thanks, you\'re totally right, I\'ll look into it!',
'Just fixed the issue on a new edit, thanks.',
`= Why I think you are stupid
Here's my essay with irrefutable proof, notably: <I don't think this is correct>.
== I don't think this is correct.
Consider what happens when $\\sqrt{a + b} > 0$`,
`Ah, maybe. But are you sure that the sum of:
\`\`
f() + 3*g()
\`\`
is enough to make the loop terminate?
`,
]
let todo_visit = articleData.map(a => [null, a])
let articleDataCount = 0
while (todo_visit.length > 0) {
let [parentEntry, entry] = todo_visit.pop()
const { children, opts } = expandArticleDataEntry(entry)
entry[2] = opts
opts.parentEntry = parentEntry
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i]
todo_visit.push([entry, child]);
if (i > 0) {
const { opts } = expandArticleDataEntry(child)
child[2] = opts
opts.previousSiblingEntry = children[i - 1]
}
}
articleDataCount++
}
class ArticleDataProvider {
constructor(articleData, userIdx) {
// These store the current tree transversal state across .get calls.
this.gen = 0
this.todo_visit = articleData.slice()
// Set of all entries we visited that don't have a parent.
// We will want to include those from the toplevel index.
this.toplevelTitleToEntry = {}
}
// Pre order depth first transversal to ensure that parents are created before children.
get() {
if (this.todo_visit.length === 0) {
this.todo_visit = articleData.slice()
this.gen++
}
while (this.todo_visit.length !== 0) {
let entry = this.todo_visit.pop();
entry = Object.assign({}, entry)
let title = entry[0]
if (this.gen > 0) {
title = `${title} v${this.gen}`
entry[0] = title
}
let children = entry[1]
for (let i = 0; i < children.length; i++) {
this.todo_visit.push(children[i]);
}
this.toplevelTitleToEntry[title] = entry
this.globalI++
return entry
}
}
}
async function generateDemoData(params) {
// Input Param defaults.
const nUsers = params.nUsers === undefined ? 2 : params.nUsers
const nArticlesPerUser = params.nArticlesPerUser === undefined ? articleDataCount : params.nArticlesPerUser
const nMaxIssuesPerArticle = params.nMaxIssuesPerArticle === undefined ? 3 : params.nMaxIssuesPerArticle
const nMaxCommentsPerIssue = params.nMaxCommentsPerIssue === undefined ? 3 : params.nMaxCommentsPerIssue
const nFollowsPerUser = params.nFollowsPerUser === undefined ? 2 : params.nFollowsPerUser
const nLikesPerUser = params.nLikesPerUser === undefined ? 20 : params.nLikesPerUser
const directory = params.directory
const basename = params.basename
const verbose = params.verbose === undefined ? false : params.verbose
const empty = params.empty === undefined ? false : params.empty
const clear = params.clear === undefined ? false : params.clear
const nArticles = nUsers * nArticlesPerUser
const sequelize = models.getSequelize(directory, basename);
const katex_macros = back_js.preloadKatex()
await models.sync(sequelize, { force: empty || clear })
if (!empty) {
const sourceRoot = path.join(__dirname, 'tmp', 'demo')
fs.rmSync(sourceRoot, { recursive: true, force: true });
if (verbose) printTimeNow = now()
if (verbose) console.error('User');
const userArgs = [];
for (let i = 0; i < nUsers; i++) {
let [displayName, image] = userData[i % userData.length]
let username = ourbigbook.titleToId(displayName)
if (i >= userData.length) {
username = `user${i}`
displayName = `User${i}`
image = undefined
}
const userArg = {
username,
displayName,
email: `user${i}@mail.com`,
verified: true,
}
if (image) {
userArg.image = image
}
sequelize.models.User.setPassword(userArg, process.env.OURBIGBOOK_DEMO_USER_PASSWORD || 'asdf')
userArgs.push(userArg)
}
const users = []
const userIdToUser = {}
for (const userArg of userArgs) {
let user = await sequelize.models.User.findOne({ where: { username: userArg.username } })
if (user) {
Object.assign(user, userArg)
await user.save()
} else {
user = await sequelize.models.User.create(userArg)
}
userIdToUser[user.id] = user
users.push(user)
}
// TODO started livelocking after we started creating index articles on hooks.
//const users = await sequelize.models.User.bulkCreate(
// userArgs,
// {
// validate: true,
// individualHooks: true,
// }
//)
if (verbose) printTime()
if (verbose) console.error('UserFollowUser');
for (let i = 0; i < nUsers; i++) {
let nFollowsPerUserEffective = nUsers < nFollowsPerUser ? nUsers : nFollowsPerUser
for (var j = 0; j < nFollowsPerUserEffective; j++) {
const follower = users[i]
const followed = users[(i + 1 + j) % nUsers]
if (!(await follower.hasFollow(followed))) {
await follower.addFollowSideEffects(followed)
}
}
}
if (verbose) printTime()
if (verbose) console.error('Article');
const articleDataProviders = {}
const articleIdToArticle = {}
for (let userIdx = 0; userIdx < nUsers; userIdx++) {
let authorId = users[userIdx].id
articleDataProvider = new ArticleDataProvider(articleData, userIdx)
articleDataProviders[authorId] = articleDataProvider
}
const articleArgs = [];
const toplevelTopicIds = new Set()
let dateI = 0
async function makeArticleArg(articleDataEntry, forceToplevel, i, authorId) {
const date = addDays(date0, dateI)
dateI++
articleDataEntry.articleIdx = i
let { titleSource, headerArgs, children, opts } = expandArticleDataEntry(articleDataEntry)
headerArgs = opts.headerArgs
if (headerArgs === undefined) {
headerArgs = ''
} else {
headerArgs += '\n\n'
}
let body = opts.body
if (body === undefined) {
body = makeBody(titleSource)
}
const id_noscope = await titleToId(titleSource)
toplevelTopicIds.add(id_noscope)
return {
titleSource,
authorId,
createdAt: date,
// TODO not taking effect. Appears to be because of the hook.
updatedAt: date,
bodySource: `${headerArgs}${body}`,
opts,
}
}
let i
for (i = 0; i < nArticlesPerUser; i++) {
for (let userIdx = 0; userIdx < nUsers; userIdx++) {
const authorId = users[userIdx].id
const articleDataProvider = articleDataProviders[authorId]
const articleDataEntry = articleDataProvider.get()
const articleArg = await makeArticleArg(articleDataEntry, false, i, authorId)
if (articleArg) {
articleArgs.push(articleArg)
}
}
}
//// Sort first by topic id, and then by user id to mix up votes a little:
//// otherwise user0 gets all votes, then user1, and so on.
//articleArgs.sort((a, b) => {
// if (a.title < b.title) {
// return -1
// } else if(a.title > b.title) {
// return 1
// } else if(a.authorId < b.authorIdtitle) {
// return -1
// } else if(a.authorId > b.authorIdtitle) {
// return 1
// } else {
// return 0;
// }
//})
const articles = []
for (const render of [false, true]) {
let articleId = 0
let i = 0
let pref
if (verbose) {
if (render) {
pref = 'render'
} else {
pref = 'extract_ids'
}
}
for (const articleArg of articleArgs) {
const msg = `${pref}: ${i}/${articleArgs.length}: ${userIdToUser[articleArg.authorId].username}/${articleArg.titleSource}`
if (verbose) console.error(msg);
const author = userIdToUser[articleArg.authorId]
const opts = articleArg.opts
let parentId
{
const parentEntry = opts.parentEntry
if (parentEntry) {
;({ opts: parentOpts } = expandArticleDataEntry(parentEntry))
parentId = `${ourbigbook.AT_MENTION_CHAR}${author.username}/${parentOpts.topicId}`
} else {
parentId = `${ourbigbook.AT_MENTION_CHAR}${author.username}`
}
}
const before = now();
const { articles: newArticles, extra_returns } = await convert.convertArticle({
author,
bodySource: articleArg.bodySource,
convertOptionsExtra: { katex_macros },
enforceMaxArticles: false,
parentId,
render,
sequelize,
titleSource: articleArg.titleSource,
})
const after = now();
opts.topicId = extra_returns.context.header_tree.children[0].ast.id.substr(
ourbigbook.AT_MENTION_CHAR.length + author.username.length + 1)
if (verbose) console.error(`${msg} finished in ${after - before}ms`);
for (const article of newArticles) {
articleIdToArticle[article.id] = article
articles.push(article)
articleId++
}
i++
}
}
// Write files to filesystem.
// https://docs.ourbigbook.com/demo-data-local-file-output
for (const user of users) {
const articles = (await sequelize.models.Article.getArticles({
sequelize,
count: false,
author: user.username,
limit: nArticlesPerUser + 1,
}))
for (const article of articles) {
const slugParse = path.parse(article.slug)
let outdir, outbase_noext
if (slugParse.dir) {
outdir = path.join(sourceRoot, slugParse.dir)
outbase_noext = slugParse.base
} else {
// Toplevel README.
outdir = path.join(sourceRoot, slugParse.base)
outbase_noext = ourbigbook.README_BASENAME_NOEXT
}
fs.mkdirSync(outdir, { recursive: true })
const outpath = path.join(outdir, outbase_noext + '.' + ourbigbook.OURBIGBOOK_EXT)
const file = article.file
fs.writeFileSync(outpath, await article.getSourceExport());
}
fs.writeFileSync(path.join(sourceRoot, user.username, ourbigbook.OURBIGBOOK_JSON_BASENAME), '{}\n');
}
// TODO This was livelocking (taking a very long time, live querries)
// due to update_database_after_convert on the hook it would be good to investigate it.
// Just convereted to the regular for loop above instead.
//const articles = await sequelize.models.Article.bulkCreate(
// articleArgs,
// {
// validate: true,
// individualHooks: true,
// }
//)
if (verbose) printTime()
if (verbose) console.error('Like');
let articleIdx = 0
for (let i = 0; i < nUsers; i++) {
const user = users[i]
for (let j = 0; j < nLikesPerUser; j++) {
const article = articles[(i * j) % nArticles];
if (
article
&& article.file.authorId !== user.id
) {
if (!(await user.hasLikedArticle(article))) {
await user.addArticleLikeSideEffects(article)
}
if (!(await user.hasFollowedArticle(article))) {
await user.addArticleFollowSideEffects(article)
}
}
}
}
// 0.5s faster than the addArticleLikeSideEffects version, total run 7s.
//let articleIdx = 0
//const likeArgs = []
//for (let i = 0; i < nUsers; i++) {
// const userId = users[i].id
// for (var j = 0; j < nLikesPerUser; j++) {
// likeArgs.push({
// userId: userId,
// articleId: articles[articleIdx % nArticles].id,
// })
// articleIdx += 1
// }
//}
//await sequelize.models.UserLikeArticle.bulkCreate(likeArgs)
if (verbose) printTime()
if (verbose) console.error('Issue');
const issues = [];
let issueIdx = 0;
await sequelize.models.Issue.destroy({ where: { authorId: users.map(user => user.id) } })
for (let i = 0; i < nArticles; i++) {
let articleIssueIdx = 0;
const article = articles[i]
for (var j = 0; j < (i % (nMaxIssuesPerArticle + 1)); j++) {
if (verbose) console.error(`${article.slug}#${articleIssueIdx}`)
const [titleSource, bodySource] = issueData[issueIdx % issueData.length]
const issue = await convert.convertIssue({
article,
bodySource,
number: articleIssueIdx + 1,
sequelize,
titleSource,
user: users[issueIdx % nUsers],
})
issue.article = article
issues.push(issue)
issueIdx++
articleIssueIdx++
}
}
const nIssues = issueIdx
if (verbose) printTime()
if (verbose) console.error('Comment');
const comments = [];
let commentIdx = 0;
await sequelize.models.Comment.destroy({ where: { authorId: users.map(user => user.id) } })
for (let i = 0; i < nIssues; i++) {
let issueCommentIdx = 0;
const issue = issues[i]
for (var j = 0; j < (i % (nMaxCommentsPerIssue + 1)); j++) {
if (verbose) console.error(`${articleIdToArticle[issue.articleId].slug}#${issue.number}#${issueCommentIdx}`)
const source = commentData[commentIdx % commentData.length]
const comment = await convert.convertComment({
issue,
source,
number: issueCommentIdx + 1,
sequelize,
user: users[commentIdx % nUsers],
})
comments.push(comment)
commentIdx++
issueCommentIdx++
}
}
if (verbose) printTime()
}
return sequelize
}
exports.generateDemoData = generateDemoData
function expandArticleDataEntry(articleDataEntry) {
let titleSource, children, opts
if (articleDataEntry === undefined) {
titleSource = `My title ${articleDataEntry.articleIdx * (userIdx + 1)}`
children = []
opts = {}
} else {
titleSource = articleDataEntry[0]
children = articleDataEntry[1]
opts = articleDataEntry[2] || {}
}
return { titleSource, children, opts }
}
function makeBody(titleSource) {
return `This is a section about ${titleSource}!
${titleSource} is a very important subject about which there is a lot to say.
For example, this sentence. And then another one.
`
/*
`This is a section about ${titleSource}!
${refsString}\\i[Italic]
\\b[Bold]
http://example.com[External link]
Inline code: \`int main() { return 1; }\`
Code block:
\`\`
function myFunc() {
return 1;
}
\`\`
Inline math: $\\sqrt{1 + 1}$
Block math and a reference to it: \\x[equation-in-${id_noscope}]:
$$\\frac{1}{\\sqrt{2}}$$\{id=equation-in-${id_noscope}}
Block quote:
\\Q[
To be or not to be.
That is the question.
]
List:
* item 1
* item 2
* item 3
Table:
|| String col
|| Integer col
|| Float col
| ab
| 2
| 10.1
| a
| 10
| 10.2
| c
| 2
| 3.4
| c
| 3
| 3.3
Reference to the following image: \\x[image-my-xi-chrysanthemum-${id_noscope}].
\\Image[https://raw.githubusercontent.com/cirosantilli/media/master/Chrysanthemum_Xi_Jinping_with_black_red_liusi_added_by_Ciro_Santilli.jpg]
{title=Xi Chrysanthemum is a very nice image}
{id=image-my-xi-chrysanthemum-${id_noscope}}
{source=https://commons.wikimedia.org/wiki/File:Lotus_flower_(978659).jpg}
An YouTube video: \\x[video-sample-youtube-video-in-${id_noscope}].
\\Video[https://youtube.com/watch?v=YeFzeNAHEhU&t=38]
{title=Sample YouTube video in ${titleSource}}${includesString}
`,
*/
}
async function titleToId(titleSource) {
return ourbigbook.titleToId(
await ourbigbook.convert(
titleSource,
{ output_format: ourbigbook.OUTPUT_FORMAT_ID }
)
)
}