2021-12-19 09:53:19 +02:00
import { readFileSync , readFile , mkdirpSync , writeFileSync , remove , copy , pathExistsSync } from 'fs-extra' ;
import { rootDir } from '../tool-utils' ;
2021-07-10 12:16:13 +02:00
import { pressCarouselItems } from './utils/pressCarousel' ;
import { getMarkdownIt , loadMustachePartials , markdownToPageHtml , renderMustache } from './utils/render' ;
2022-11-28 18:16:32 +02:00
import { AssetUrls , Env , Partials , PlanPageParams , Sponsors , TemplateParams } from './utils/types' ;
2022-04-07 16:35:15 +02:00
import { createFeatureTableMd , getPlans , loadStripeConfig } from '@joplin/lib/utils/joplinCloud' ;
2022-06-03 13:31:42 +02:00
import { stripOffFrontMatter } from './utils/frontMatter' ;
2021-12-19 09:53:19 +02:00
import { dirname , basename } from 'path' ;
2022-02-24 21:35:28 +02:00
import { readmeFileTitle , replaceGitHubByWebsiteLinks } from './utils/parser' ;
import { extractOpenGraphTags } from './utils/openGraph' ;
2022-04-09 13:32:19 +02:00
import { readCredentialFileJson } from '@joplin/lib/utils/credentialFiles' ;
2022-06-03 13:31:42 +02:00
import { getNewsDateString } from './utils/news' ;
2022-11-28 18:16:32 +02:00
import { parsePoFile , parseTranslations , Translations } from '../utils/translation' ;
import { countryCodeOnly , setLocale } from '@joplin/lib/locale' ;
import applyTranslations from './utils/applyTranslations' ;
2021-12-17 19:37:01 +02:00
2022-04-09 13:32:19 +02:00
interface BuildConfig {
env : Env ;
}
const buildConfig = readCredentialFileJson < BuildConfig > ( 'website-build.json' , {
env : Env.Prod ,
} ) ;
2021-07-10 12:16:13 +02:00
const glob = require ( 'glob' ) ;
const path = require ( 'path' ) ;
2022-11-13 13:37:05 +02:00
const md5File = require ( 'md5-file' ) ;
2021-12-19 09:53:19 +02:00
const docDir = ` ${ dirname ( dirname ( dirname ( dirname ( __dirname ) ) ) ) } /joplin-website/docs ` ;
if ( ! pathExistsSync ( docDir ) ) throw new Error ( ` Doc directory does not exist: ${ docDir } ` ) ;
2021-07-10 12:16:13 +02:00
const websiteAssetDir = ` ${ rootDir } /Assets/WebsiteAssets ` ;
2021-12-21 20:53:36 +02:00
const readmeDir = ` ${ rootDir } /readme ` ;
2021-12-17 19:37:01 +02:00
const mainTemplateHtml = readFileSync ( ` ${ websiteAssetDir } /templates/main-new.mustache ` , 'utf8' ) ;
const frontTemplateHtml = readFileSync ( ` ${ websiteAssetDir } /templates/front.mustache ` , 'utf8' ) ;
const plansTemplateHtml = readFileSync ( ` ${ websiteAssetDir } /templates/plans.mustache ` , 'utf8' ) ;
2022-04-09 13:32:19 +02:00
const stripeConfig = loadStripeConfig ( buildConfig . env , ` ${ rootDir } /packages/server/stripeConfig.json ` ) ;
2021-07-10 12:16:13 +02:00
const partialDir = ` ${ websiteAssetDir } /templates/partials ` ;
2021-12-18 17:45:59 +02:00
const discussLink = 'https://discourse.joplinapp.org/c/news/9' ;
2021-07-10 12:16:13 +02:00
let tocMd_ : string = null ;
let tocHtml_ : string = null ;
const tocRegex_ = /<!-- TOC -->([^]*)<!-- TOC -->/ ;
function tocMd() {
if ( tocMd_ ) return tocMd_ ;
2021-12-17 19:37:01 +02:00
const md = readFileSync ( ` ${ rootDir } /README.md ` , 'utf8' ) ;
2021-07-10 12:16:13 +02:00
const toc = md . match ( tocRegex_ ) ;
tocMd_ = toc [ 1 ] ;
return tocMd_ ;
}
const donateLinksRegex_ = /<!-- DONATELINKS -->([^]*)<!-- DONATELINKS -->/ ;
async function getDonateLinks() {
2021-12-17 19:37:01 +02:00
const md = await readFile ( ` ${ rootDir } /README.md ` , 'utf8' ) ;
2021-07-10 12:16:13 +02:00
const matches = md . match ( donateLinksRegex_ ) ;
if ( ! matches ) throw new Error ( 'Cannot fetch donate links' ) ;
return ` <div class="donate-links"> \ n \ n ${ matches [ 1 ] . trim ( ) } \ n \ n</div> ` ;
}
function tocHtml() {
if ( tocHtml_ ) return tocHtml_ ;
const markdownIt = getMarkdownIt ( ) ;
let md = tocMd ( ) ;
md = md . replace ( /# Table of contents/ , '' ) ;
md = replaceGitHubByWebsiteLinks ( md ) ;
tocHtml_ = markdownIt . render ( md ) ;
tocHtml_ = ` <div> ${ tocHtml_ } </div> ` ;
return tocHtml_ ;
}
2021-10-25 17:49:21 +02:00
const baseUrl = '' ;
const cssBasePath = ` ${ websiteAssetDir } /css ` ;
const cssBaseUrl = ` ${ baseUrl } /css ` ;
const jsBasePath = ` ${ websiteAssetDir } /js ` ;
const jsBaseUrl = ` ${ baseUrl } /js ` ;
2021-07-10 12:16:13 +02:00
2021-10-25 17:49:21 +02:00
async function getAssetUrls ( ) : Promise < AssetUrls > {
return {
css : {
fontawesome : ` ${ cssBaseUrl } /fontawesome-all.min.css?h= ${ await md5File ( ` ${ cssBasePath } /fontawesome-all.min.css ` ) } ` ,
site : ` ${ cssBaseUrl } /site.css?h= ${ await md5File ( ` ${ cssBasePath } /site.css ` ) } ` ,
} ,
js : {
script : ` ${ jsBaseUrl } /script.js?h= ${ await md5File ( ` ${ jsBasePath } /script.js ` ) } ` ,
} ,
} ;
}
function defaultTemplateParams ( assetUrls : AssetUrls ) : TemplateParams {
2021-07-10 12:16:13 +02:00
return {
2022-04-09 13:32:19 +02:00
env : buildConfig.env ,
2021-10-25 17:49:21 +02:00
baseUrl ,
2021-07-10 12:16:13 +02:00
imageBaseUrl : ` ${ baseUrl } /images ` ,
2021-10-25 17:49:21 +02:00
cssBaseUrl ,
jsBaseUrl ,
2021-07-10 12:16:13 +02:00
tocHtml : tocHtml ( ) ,
yyyy : ( new Date ( ) ) . getFullYear ( ) . toString ( ) ,
templateHtml : mainTemplateHtml ,
forumUrl : 'https://discourse.joplinapp.org/' ,
showToc : true ,
showImproveThisDoc : true ,
2021-07-19 11:58:21 +02:00
showJoplinCloudLinks : true ,
2021-07-10 12:16:13 +02:00
navbar : {
isFrontPage : false ,
} ,
2021-10-25 17:49:21 +02:00
assetUrls ,
2022-02-24 21:35:28 +02:00
openGraph : null ,
2021-07-10 12:16:13 +02:00
} ;
}
function renderPageToHtml ( md : string , targetPath : string , templateParams : TemplateParams ) {
// Remove the header because it's going to be added back as HTML
md = md . replace ( /# Joplin\n/ , '' ) ;
templateParams = {
2021-10-25 17:49:21 +02:00
. . . defaultTemplateParams ( templateParams . assetUrls ) ,
2021-07-10 12:16:13 +02:00
. . . templateParams ,
} ;
2022-03-03 16:06:37 +02:00
templateParams . showBottomLinks = templateParams . showImproveThisDoc ;
2021-12-18 17:45:59 +02:00
2021-07-10 12:16:13 +02:00
const title = [ ] ;
if ( ! templateParams . title ) {
title . push ( 'Joplin - an open source note taking and to-do application with synchronisation capabilities' ) ;
} else {
title . push ( templateParams . title ) ;
title . push ( 'Joplin' ) ;
}
md = replaceGitHubByWebsiteLinks ( md ) ;
if ( templateParams . donateLinksMd ) {
md = ` ${ templateParams . donateLinksMd } \ n \ n ${ md } ` ;
}
templateParams . pageTitle = title . join ( ' | ' ) ;
const html = templateParams . contentHtml ? renderMustache ( templateParams . contentHtml , templateParams ) : markdownToPageHtml ( md , templateParams ) ;
const folderPath = dirname ( targetPath ) ;
2021-12-17 19:37:01 +02:00
mkdirpSync ( folderPath ) ;
2021-07-10 12:16:13 +02:00
2021-12-17 19:37:01 +02:00
writeFileSync ( targetPath , html ) ;
2021-07-10 12:16:13 +02:00
}
function renderFileToHtml ( sourcePath : string , targetPath : string , templateParams : TemplateParams ) {
2022-11-15 18:00:06 +02:00
try {
let md = readFileSync ( sourcePath , 'utf8' ) ;
if ( templateParams . isNews ) {
md = processNewsMarkdown ( md , sourcePath ) ;
}
md = stripOffFrontMatter ( md ) . doc ;
return renderPageToHtml ( md , targetPath , templateParams ) ;
} catch ( error ) {
error . message = ` Could not render file: ${ sourcePath } : ${ error . message } ` ;
throw error ;
2022-03-03 16:06:37 +02:00
}
2021-07-10 12:16:13 +02:00
}
function makeHomePageMd() {
2021-12-17 19:37:01 +02:00
let md = readFileSync ( ` ${ rootDir } /README.md ` , 'utf8' ) ;
2021-07-10 12:16:13 +02:00
md = md . replace ( tocRegex_ , '' ) ;
// HACK: GitHub needs the \| or the inline code won't be displayed correctly inside the table,
// while MarkdownIt doesn't and will in fact display the \. So we remove it here.
md = md . replace ( /\\\| bash/g , '| bash' ) ;
2022-01-09 19:49:37 +02:00
// We strip-off the donate links because they are added back (with proper
// classes and CSS).
md = md . replace ( donateLinksRegex_ , '' ) ;
2021-07-10 12:16:13 +02:00
return md ;
}
async function loadSponsors ( ) : Promise < Sponsors > {
const sponsorsPath = ` ${ rootDir } /packages/tools/sponsors.json ` ;
2021-12-17 19:37:01 +02:00
const output : Sponsors = JSON . parse ( await readFile ( sponsorsPath , 'utf8' ) ) ;
2021-12-19 23:13:19 +02:00
output . orgs = output . orgs . map ( o = > {
2021-07-26 19:21:19 +02:00
if ( o . urlWebsite ) o . url = o . urlWebsite ;
return o ;
2021-12-19 23:13:19 +02:00
} ) ;
2021-07-26 19:21:19 +02:00
return output ;
2021-07-10 12:16:13 +02:00
}
2022-03-03 16:06:37 +02:00
const processNewsMarkdown = ( md : string , mdFilePath : string ) : string = > {
const info = stripOffFrontMatter ( md ) ;
md = info . doc . trim ( ) ;
2022-03-10 19:43:06 +02:00
const dateString = getNewsDateString ( info , mdFilePath ) ;
2022-03-03 16:06:37 +02:00
md = md . replace ( /^# (.*)/ , ` # [ $ 1](https://github.com/laurent22/joplin/blob/dev/readme/news/ ${ path . basename ( mdFilePath ) } ) \ n \ n*Published on ** ${ dateString } *** \ n \ n ` ) ;
md += ` \ n \ n* * * \ n \ n[<i class="fab fa-discourse"></i> Discuss on the forum]( ${ discussLink } ) ` ;
return md ;
} ;
2021-12-17 19:37:01 +02:00
const makeNewsFrontPage = async ( sourceFilePaths : string [ ] , targetFilePath : string , templateParams : TemplateParams ) = > {
const maxNewsPerPage = 20 ;
const frontPageMd : string [ ] = [ ] ;
for ( const mdFilePath of sourceFilePaths ) {
let md = await readFile ( mdFilePath , 'utf8' ) ;
2022-03-03 16:06:37 +02:00
md = processNewsMarkdown ( md , mdFilePath ) ;
2021-12-17 19:37:01 +02:00
frontPageMd . push ( md ) ;
if ( frontPageMd . length >= maxNewsPerPage ) break ;
}
renderPageToHtml ( frontPageMd . join ( '\n\n* * *\n\n' ) , targetFilePath , templateParams ) ;
} ;
const isNewsFile = ( filePath : string ) : boolean = > {
return filePath . includes ( 'readme/news/' ) ;
} ;
2022-11-28 18:16:32 +02:00
const translatePartials = ( partials : Partials , languageCode : string , translations : Translations ) : Partials = > {
const output : Partials = { } ;
for ( const [ key , value ] of Object . entries ( partials ) ) {
output [ key ] = applyTranslations ( value , languageCode , translations ) ;
}
return output ;
} ;
const updatePageLanguage = ( html : string , lang : string ) : string = > {
return html . replace ( '<html lang="en-gb">' , ` <html lang=" ${ lang } "> ` ) ;
} ;
2021-07-10 12:16:13 +02:00
async function main() {
2022-11-28 18:16:32 +02:00
const supportedLocales = {
'en_GB' : {
htmlTranslations : { } ,
lang : 'en-gb' ,
} ,
'zh_CN' : {
htmlTranslations : parseTranslations ( await parsePoFile ( ` ${ websiteAssetDir } /locales/zh_CN.po ` ) ) ,
lang : 'zh-cn' ,
} ,
} ;
setLocale ( 'en_GB' ) ;
2021-12-19 09:53:19 +02:00
await remove ( ` ${ docDir } ` ) ;
await copy ( websiteAssetDir , ` ${ docDir } ` ) ;
2021-07-10 12:16:13 +02:00
const sponsors = await loadSponsors ( ) ;
const partials = await loadMustachePartials ( partialDir ) ;
2021-10-25 17:49:21 +02:00
const assetUrls = await getAssetUrls ( ) ;
2021-07-10 12:16:13 +02:00
const readmeMd = makeHomePageMd ( ) ;
2022-01-09 19:49:37 +02:00
const donateLinksMd = await getDonateLinks ( ) ;
2021-07-10 12:16:13 +02:00
// =============================================================
// HELP PAGE
// =============================================================
2022-01-09 19:49:37 +02:00
renderPageToHtml ( readmeMd , ` ${ docDir } /help/index.html ` , {
sourceMarkdownFile : 'README.md' ,
donateLinksMd ,
partials ,
sponsors ,
assetUrls ,
2022-02-24 21:35:28 +02:00
openGraph : {
title : 'Joplin documentation' ,
description : '' ,
url : 'https://joplinapp.org/help/' ,
} ,
2022-01-09 19:49:37 +02:00
} ) ;
2021-07-10 12:16:13 +02:00
// =============================================================
// FRONT PAGE
// =============================================================
2022-11-28 18:16:32 +02:00
for ( const [ localeName , locale ] of Object . entries ( supportedLocales ) ) {
setLocale ( localeName ) ;
const pathPrefix = localeName !== 'en_GB' ? ` / ${ countryCodeOnly ( localeName ) . toLowerCase ( ) } ` : '' ;
let templateHtml = updatePageLanguage ( applyTranslations ( frontTemplateHtml , localeName , locale . htmlTranslations ) , locale . lang ) ;
if ( localeName === 'zh_CN' ) templateHtml = templateHtml . replace ( /\/plans/g , '/cn/plans' ) ;
renderPageToHtml ( '' , ` ${ docDir } ${ pathPrefix } /index.html ` , {
templateHtml ,
partials : translatePartials ( partials , localeName , locale . htmlTranslations ) ,
pressCarouselRegular : {
id : 'carouselRegular' ,
items : pressCarouselItems ( ) ,
} ,
pressCarouselMobile : {
id : 'carouselMobile' ,
items : pressCarouselItems ( ) ,
} ,
sponsors ,
navbar : {
isFrontPage : true ,
} ,
showToc : false ,
assetUrls ,
openGraph : {
title : 'Joplin website' ,
description : 'Joplin, the open source note-taking application' ,
url : 'https://joplinapp.org' ,
} ,
} ) ;
}
setLocale ( 'en_GB' ) ;
2021-07-10 12:16:13 +02:00
// =============================================================
// PLANS PAGE
// =============================================================
2022-11-28 18:16:32 +02:00
for ( const [ localeName , locale ] of Object . entries ( supportedLocales ) ) {
setLocale ( localeName ) ;
2021-07-10 12:16:13 +02:00
2022-11-28 18:16:32 +02:00
const planPageFaqMd = await readFile ( ` ${ readmeDir } /faq_joplin_cloud.md ` , 'utf8' ) ;
const planPageFaqHtml = getMarkdownIt ( ) . render ( planPageFaqMd , { } ) ;
2021-07-10 12:16:13 +02:00
2022-11-28 18:16:32 +02:00
const planPageParams : PlanPageParams = {
. . . defaultTemplateParams ( assetUrls ) ,
partials : translatePartials ( partials , localeName , locale . htmlTranslations ) ,
templateHtml : applyTranslations ( plansTemplateHtml , localeName , locale . htmlTranslations ) ,
plans : getPlans ( stripeConfig ) ,
faqHtml : planPageFaqHtml ,
featureListHtml : getMarkdownIt ( ) . render ( createFeatureTableMd ( ) , { } ) ,
stripeConfig ,
} ;
2021-07-10 12:16:13 +02:00
2022-11-28 18:16:32 +02:00
const planPageContentHtml = renderMustache ( '' , planPageParams ) ;
const pathPrefix = localeName !== 'en_GB' ? ` / ${ countryCodeOnly ( localeName ) . toLowerCase ( ) } ` : '' ;
const templateParams = {
. . . defaultTemplateParams ( assetUrls ) ,
pageName : 'plans' ,
partials ,
showToc : false ,
showImproveThisDoc : false ,
contentHtml : planPageContentHtml ,
title : 'Joplin Cloud Plans' ,
} ;
templateParams . templateHtml = updatePageLanguage ( templateParams . templateHtml , locale . lang ) ;
renderPageToHtml ( '' , ` ${ docDir } ${ pathPrefix } /plans/index.html ` , templateParams ) ;
}
setLocale ( 'en_GB' ) ;
2021-07-10 12:16:13 +02:00
// =============================================================
// All other pages are generated dynamically from the
// Markdown files under /readme
// =============================================================
2021-12-21 20:53:36 +02:00
const mdFiles = glob . sync ( ` ${ readmeDir } /**/*.md ` ) . map ( ( f : string ) = > f . substr ( rootDir . length + 1 ) ) ;
2021-07-10 12:16:13 +02:00
const sources = [ ] ;
2022-02-24 21:35:28 +02:00
const makeTargetBasename = ( input : string ) : string = > {
2021-12-17 19:37:01 +02:00
if ( isNewsFile ( input ) ) {
2021-12-21 20:53:36 +02:00
const filenameNoExt = basename ( input , '.md' ) ;
2022-02-24 21:35:28 +02:00
return ` news/ ${ filenameNoExt } /index.html ` ;
2021-12-17 19:37:01 +02:00
} else {
2021-12-21 20:53:36 +02:00
// Input is for example "readme/spec/interop_with_frontmatter.md",
// and we need to convert it to
// "docs/spec/interop_with_frontmatter/index.html" and prefix it
// with the website repo full path.
2022-02-24 21:35:28 +02:00
let s = input ;
2022-02-09 17:24:08 +02:00
if ( s . endsWith ( 'index.md' ) ) {
s = s . replace ( /index\.md/ , 'index.html' ) ;
} else {
s = s . replace ( /\.md/ , '/index.html' ) ;
}
s = s . replace ( /readme\// , '' ) ;
return s ;
2021-12-17 19:37:01 +02:00
}
} ;
2022-02-24 21:35:28 +02:00
const makeTargetFilePath = ( input : string ) : string = > {
return ` ${ docDir } / ${ makeTargetBasename ( input ) } ` ;
} ;
const makeTargetUrl = ( input : string ) = > {
return ` https://joplinapp.org/ ${ makeTargetBasename ( input ) } ` ;
} ;
2021-12-17 19:37:01 +02:00
const newsFilePaths : string [ ] = [ ] ;
2021-07-10 12:16:13 +02:00
for ( const mdFile of mdFiles ) {
const title = await readmeFileTitle ( ` ${ rootDir } / ${ mdFile } ` ) ;
2021-12-17 19:37:01 +02:00
const targetFilePath = makeTargetFilePath ( mdFile ) ;
2022-02-24 21:35:28 +02:00
const openGraph = await extractOpenGraphTags ( mdFile , makeTargetUrl ( mdFile ) ) ;
2021-12-17 19:37:01 +02:00
const isNews = isNewsFile ( mdFile ) ;
if ( isNews ) newsFilePaths . push ( mdFile ) ;
2021-07-10 12:16:13 +02:00
sources . push ( [ mdFile , targetFilePath , {
title : title ,
donateLinksMd : mdFile === 'readme/donate.md' ? '' : donateLinksMd ,
2021-12-17 19:37:01 +02:00
showToc : mdFile !== 'readme/download.md' && ! isNews ,
2022-02-24 21:35:28 +02:00
openGraph ,
2021-07-10 12:16:13 +02:00
} ] ) ;
}
for ( const source of sources ) {
source [ 2 ] . sourceMarkdownFile = source [ 0 ] ;
source [ 2 ] . sourceMarkdownName = path . basename ( source [ 0 ] , path . extname ( source [ 0 ] ) ) ;
2021-12-18 17:31:08 +02:00
const sourceFilePath = ` ${ rootDir } / ${ source [ 0 ] } ` ;
2021-12-19 09:53:19 +02:00
const targetFilePath = source [ 1 ] ;
2021-12-18 17:45:59 +02:00
const isNews = isNewsFile ( sourceFilePath ) ;
2021-12-18 17:31:08 +02:00
2021-12-19 09:53:19 +02:00
renderFileToHtml ( sourceFilePath , targetFilePath , {
2021-07-10 12:16:13 +02:00
. . . source [ 2 ] ,
templateHtml : mainTemplateHtml ,
2021-12-18 17:45:59 +02:00
pageName : isNews ? 'news-item' : '' ,
showImproveThisDoc : ! isNews ,
2022-03-03 16:06:37 +02:00
isNews ,
2021-07-10 12:16:13 +02:00
partials ,
2021-10-25 17:49:21 +02:00
assetUrls ,
2021-07-10 12:16:13 +02:00
} ) ;
}
2021-12-17 19:37:01 +02:00
newsFilePaths . sort ( ( a , b ) = > {
return a . toLowerCase ( ) > b . toLowerCase ( ) ? - 1 : + 1 ;
} ) ;
2021-12-19 09:53:19 +02:00
await makeNewsFrontPage ( newsFilePaths , ` ${ docDir } /news/index.html ` , {
2021-12-17 19:37:01 +02:00
. . . defaultTemplateParams ( assetUrls ) ,
2022-02-24 21:35:28 +02:00
title : 'What\'s new' ,
2021-12-17 19:58:49 +02:00
pageName : 'news' ,
2021-12-17 19:37:01 +02:00
partials ,
showToc : false ,
showImproveThisDoc : false ,
donateLinksMd ,
2022-02-24 21:35:28 +02:00
openGraph : {
title : 'Joplin - what\'s new' ,
description : 'News about the Joplin open source application' ,
url : 'https://joplinapp.org/news/' ,
} ,
2021-12-17 19:37:01 +02:00
} ) ;
2022-11-22 20:16:57 +02:00
2022-11-28 18:16:32 +02:00
// setLocale('zh_CN');
// const translations = parseTranslations(await parsePoFile(`${websiteAssetDir}/locales/zh_CN.po`));
// await processTranslations(`${docDir}/index.html`, `${docDir}/cn/index.html`, 'zh-cn', translations);
// await processTranslations(`${docDir}/plans/index.html`, `${docDir}/cn/plans/index.html`, 'zh-cn', translations);
// setLocale('en_GB');
2021-07-10 12:16:13 +02:00
}
main ( ) . catch ( ( error ) = > {
console . error ( error ) ;
2021-12-17 16:08:52 +02:00
process . exit ( 1 ) ;
2021-07-10 12:16:13 +02:00
} ) ;