aboutsummaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/baseline.sh6
-rwxr-xr-xscripts/codacy_coverage.sh10
-rw-r--r--scripts/debug_rebuilds/README.md11
-rwxr-xr-xscripts/debug_rebuilds/step1_build_and_dumpdb.py50
-rwxr-xr-xscripts/debug_rebuilds/step2_analyze_py_files.py58
-rwxr-xr-xscripts/document_path_handlers.py14
-rwxr-xr-xscripts/generate_conf.py9
-rwxr-xr-xscripts/getwheelhouse.sh7
-rwxr-xr-xscripts/import_po.py10
-rwxr-xr-xscripts/jinjify.py51
-rwxr-xr-xscripts/langmatrix.py20
-rwxr-xr-xscripts/release23
-rwxr-xr-xscripts/snapcraft/nikola.sh21
-rwxr-xr-xscripts/update-bower.sh57
-rwxr-xr-xscripts/update-npm-assets.sh59
15 files changed, 286 insertions, 120 deletions
diff --git a/scripts/baseline.sh b/scripts/baseline.sh
index 2553a07..d4bf2ba 100755
--- a/scripts/baseline.sh
+++ b/scripts/baseline.sh
@@ -1,9 +1,9 @@
#!/bin/bash
PYVER=$(scripts/getpyver.py short)
-if [[ $PYVER == '3.5' || $PYVER == '2.7' ]]; then
+if [[ $PYVER == '3.8' ]]; then
if [[ "$1" == "check" ]]; then
echo -e "\033[36m>> Downloading baseline for $PYVER...\033[0m"
- # we only support 2.7 and 3.5
+ # we only support 3.8
wget https://github.com/getnikola/invariant-builds/archive/v$PYVER'.zip'
unzip -q 'v'$PYVER'.zip'
rm -rf baseline$PYVER
@@ -17,7 +17,7 @@ fi
nikola init -qd nikola-baseline-build
cd nikola-baseline-build
cp ../tests/data/1-nolinks.rst posts/1.rst
-rm "pages/creating-a-theme.rst" "pages/extending.txt" "pages/internals.txt" "pages/manual.rst" "pages/social_buttons.txt" "pages/theming.rst" "pages/path_handlers.txt" "pages/charts.txt"
+rm "pages/creating-a-theme.rst" "pages/extending.rst" "pages/internals.rst" "pages/manual.rst" "pages/social_buttons.rst" "pages/theming.rst" "pages/path_handlers.rst" "pages/charts.rst"
LC_ALL='en_US.UTF-8' PYTHONHASHSEED=0 nikola build --invariant
if [[ "$1" == "check" ]]; then
echo -e "\033[36m>> Testing baseline...\033[0m"
diff --git a/scripts/codacy_coverage.sh b/scripts/codacy_coverage.sh
new file mode 100755
index 0000000..3ba43ee
--- /dev/null
+++ b/scripts/codacy_coverage.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+PYVER=$(scripts/getpyver.py short)
+if [[ $PYVER == '3.6' ]]; then
+ if [[ "x$CODACY_PROJECT_TOKEN" == "x" ]]; then
+ echo "Warning: Codacy token not available (PR from non-member), unable to send coverage info."
+ exit 0
+ fi
+ coverage xml
+ python-codacy-coverage -r coverage.xml
+fi
diff --git a/scripts/debug_rebuilds/README.md b/scripts/debug_rebuilds/README.md
new file mode 100644
index 0000000..05305a9
--- /dev/null
+++ b/scripts/debug_rebuilds/README.md
@@ -0,0 +1,11 @@
+To debug unexpected Nikola rebuilds:
+
+1. In `nikola.utils.config_changed._calc_digest`, uncomment the line that says `self._write_into_debug_db(digest, data)`
+2. Create a copy of your site source.
+3. Run `python step1_build_and_dumpdb.py`. (It will delete cache/output/db, build the site twice, and write dumpdb to .py files)
+4. Run `python step2_analyze_py_files.py | tee analysis.txt`. It will compare the two .py files, using `cc_debug.sqlite3` and `{first,second}_dump.py`.
+5. Compare the produced dictionaries. Note that you will probably need a character-level diff tool, <https://prettydiff.com/> is pretty good as long as you change CSS for `li.replace` to `word-break: break-all; white-space: pre-wrap;`
+
+Copyright © 2019-2020, Chris Warrick.
+Portions Copyright © Eduardo Nafuel Schettino and Doit Contributors.
+License of .py files is MIT (same as Nikola)
diff --git a/scripts/debug_rebuilds/step1_build_and_dumpdb.py b/scripts/debug_rebuilds/step1_build_and_dumpdb.py
new file mode 100755
index 0000000..69f952d
--- /dev/null
+++ b/scripts/debug_rebuilds/step1_build_and_dumpdb.py
@@ -0,0 +1,50 @@
+import dbm
+import json
+import subprocess
+import sys
+
+
+def dbm_iter(db):
+ # try dictionary interface - ok in python2 and dumbdb
+ try:
+ return db.items()
+ except Exception:
+ # try firstkey/nextkey - ok for py3 dbm.gnu
+ def iter_gdbm(db):
+ k = db.firstkey()
+ while k is not None:
+ yield k, db[k]
+ k = db.nextkey(k)
+ return iter_gdbm(db)
+
+
+def dumpdb():
+ with dbm.open('.doit.db') as data:
+ return {key: json.loads(value_str.decode('utf-8'))
+ for key, value_str in dbm_iter(data)}
+
+
+print_ = print
+
+
+def print(*args, **kwargs):
+ print_(*args, file=sys.stdout)
+ sys.stdout.flush()
+
+
+print("==> Removing stuff...")
+subprocess.call(['rm', '-rf', '.doit.db', 'output', 'cache', 'cc_debug.sqlite3'])
+print("==> Running first build...")
+subprocess.call(['nikola', 'build'])
+print("==> Fetching database...")
+first = dumpdb()
+print("==> Running second build...")
+subprocess.call(['nikola', 'build'])
+print("==> Fetching database...")
+second = dumpdb()
+print("==> Saving dumps...")
+with open('first_dump.py', 'w', encoding='utf-8') as fh:
+ fh.write(repr(first))
+
+with open('second_dump.py', 'w', encoding='utf-8') as fh:
+ fh.write(repr(second))
diff --git a/scripts/debug_rebuilds/step2_analyze_py_files.py b/scripts/debug_rebuilds/step2_analyze_py_files.py
new file mode 100755
index 0000000..cb5c954
--- /dev/null
+++ b/scripts/debug_rebuilds/step2_analyze_py_files.py
@@ -0,0 +1,58 @@
+import sqlite3
+import sys
+print_ = print
+
+
+def print(*args, **kwargs):
+ print_(*args, file=sys.stdout)
+ sys.stdout.flush()
+
+
+with open('first_dump.py', 'r', encoding='utf-8') as fh:
+ first = eval(fh.read())
+
+with open('second_dump.py', 'r', encoding='utf-8') as fh:
+ second = eval(fh.read())
+
+if len(first) != len(second):
+ print(" [!] Databases differ in size.")
+ for k in first:
+ if k not in second:
+ print(" Item", k, "not found in second database.")
+ for k in second:
+ if k not in first:
+ print(" Item", k, "not found in first database.")
+
+conn = sqlite3.connect("cc_debug.sqlite3")
+
+
+def get_from_db(value):
+ cursor = conn.cursor()
+ try:
+ cursor.execute("SELECT json_data FROM hashes WHERE hash = ?", (value,))
+ return cursor.fetchone()[0]
+ except Exception:
+ print(" [!] Cannot find", value, "in database.")
+ return None
+
+
+if first == second:
+ print("==> Both files are identical.")
+ exit(0)
+
+VAL_KEY = '_values_:' # yes, ends with a colon
+for k in first:
+ fk, sk = first[k], second[k]
+ try:
+ first_values, second_values = fk[VAL_KEY], sk[VAL_KEY]
+ except KeyError:
+ print(" [!] Values not found for,", k)
+ continue
+
+ if first_values != second_values:
+ print(" -> Difference:", k)
+ for vk in first_values:
+ fv, sv = first_values[vk], second_values[vk]
+ if fv != sv:
+ print(" first :", fv, get_from_db(fv))
+ print(" second:", sv, get_from_db(sv))
diff --git a/scripts/document_path_handlers.py b/scripts/document_path_handlers.py
index 4c2e378..d86309a 100755
--- a/scripts/document_path_handlers.py
+++ b/scripts/document_path_handlers.py
@@ -1,6 +1,5 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
-from __future__ import print_function, unicode_literals
from nikola import nikola
n = nikola.Nikola()
n.init_plugins()
@@ -9,8 +8,15 @@ print(""".. title: Path Handlers for Nikola
.. slug: path-handlers
.. author: The Nikola Team
-Nikola supports special links with the syntax ``link://kind/name``. In the templates you can also
-use ``_link(kind, name)`` Here is the description for all the supported kinds.
+.. DO NOT EDIT, this file is auto-generated by scripts/document_path_handlers.py
+
+Nikola supports special links with the syntax ``link://kind/name``. In
+templates you can also use ``_link(kind, name)``. You can add query strings
+(``?key=value``) for extra arguments, or pass keyword arguments to ``_link`` in
+templates (support and behavior depends on path handlers themselves). Fragments
+(``#anchor``) will be appended to the transformed link.
+
+Here are the descriptions for all the supported kinds.
.. class:: dl-horizontal
""")
diff --git a/scripts/generate_conf.py b/scripts/generate_conf.py
deleted file mode 100755
index 37a3a94..0000000
--- a/scripts/generate_conf.py
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env python
-# Generate a conf.py file from the template, using default settings.
-
-import nikola.plugins.command.init
-
-try:
- print(nikola.plugins.command.init.CommandInit.create_configuration_to_string())
-except:
- print(nikola.plugins.command.init.CommandInit.create_configuration_to_string().encode('utf-8'))
diff --git a/scripts/getwheelhouse.sh b/scripts/getwheelhouse.sh
deleted file mode 100755
index 911ffbd..0000000
--- a/scripts/getwheelhouse.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-for i in $@; do
- wget https://github.com/getnikola/wheelhouse/archive/v$i'.zip'
- unzip 'v'$i'.zip'
- pip install --use-wheel --no-index --find-links=wheelhouse-$i lxml Pillow ipython
- rm -rf wheelhouse-$i 'v'$i'.zip'
-done
diff --git a/scripts/import_po.py b/scripts/import_po.py
index 2f887c5..3f2c984 100755
--- a/scripts/import_po.py
+++ b/scripts/import_po.py
@@ -1,16 +1,18 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Download translations from transifex and regenerate files."""
-from __future__ import unicode_literals, print_function
import io
from glob import glob
import os
import sys
import polib
-if 'nopull' not in sys.argv:
+if 'nopull' not in sys.argv or '--nopull' in sys.argv:
os.system("tx pull -a")
+elif '-h' in sys.argv or '--help' in sys.argv:
+ print("Internal use only. Takes optional 'nopull' argument to prevent pulling from Transifex.")
+ exit()
trans_files = glob(os.path.join('translations', 'nikola.messages', '*.po'))
for fname in trans_files:
@@ -20,7 +22,7 @@ for fname in trans_files:
'messages', 'messages_{0}.py'.format(lang))
po = polib.pofile(fname)
lines = """# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+\"\"\"Autogenerated file, do not edit. Submit translations on Transifex.\"\"\"
MESSAGES = {""".splitlines()
lines2 = []
diff --git a/scripts/jinjify.py b/scripts/jinjify.py
index 8f14323..629ffb1 100755
--- a/scripts/jinjify.py
+++ b/scripts/jinjify.py
@@ -1,4 +1,6 @@
#!/usr/bin/env python
+"""Script to convert templates from Mako to Jinja2."""
+
import io
import glob
import sys
@@ -8,11 +10,10 @@ import json
import shutil
import tempfile
-import colorama
import jinja2
dumb_replacements = [
- ["{% if any(post.is_mathjax for post in posts) %}", '{% if posts|selectattr("is_mathjax")|list %}'],
+ ["{% if any(post.has_math for post in posts) %}", '{% if posts|selectattr("has_math")|list %}'],
["json.dumps(title)", "title|tojson"],
["{{ parent.extra_head() }}", "{{ super() }}"],
["{{ parent.content() }}", "{{ super() }}"],
@@ -22,6 +23,10 @@ dumb_replacements = [
["fb: http://ogp.me/ns/fb# \\", "fb: http://ogp.me/ns/fb#"],
['dir="rtl" \\', 'dir="rtl"'],
['sorted(translations)', 'translations|sort'],
+ ['abs(i - current_page)', '(i - current_page)|abs'],
+ ['loop.index', 'loop.index0'],
+ ['is None', 'is none'],
+ ['is not None', 'is not none'],
]
dumber_replacements = [
@@ -39,11 +44,11 @@ def jinjify(in_theme, out_theme):
out_templates_path = os.path.join(out_theme, "templates")
try:
os.makedirs(out_templates_path)
- except:
+ except Exception:
pass
lookup = jinja2.Environment()
lookup.filters['tojson'] = json.dumps
- lookup.loader = jinja2.FileSystemLoader([out_templates_path], encoding='utf-8')
+ lookup.loader = jinja2.FileSystemLoader([out_templates_path], encoding='utf-8-sig')
for template in glob.glob(os.path.join(in_templates_path, "*.tmpl")):
out_template = os.path.join(out_templates_path, os.path.basename(template))
with io.open(template, "r", encoding="utf-8") as inf:
@@ -73,21 +78,17 @@ def jinjify(in_theme, out_theme):
child = os.path.basename(out_theme.rstrip('/'))
mappings = {
'base-jinja': 'base',
- 'bootstrap3-jinja': 'base-jinja',
+ 'bootstrap4-jinja': 'base-jinja',
}
if child in mappings:
parent = mappings[child]
- with io.open(os.path.join(out_theme, "parent"), "w+", encoding='utf-8') as outf:
- outf.write(u'{0}\n'.format(parent))
-
- with io.open(os.path.join(out_theme, "engine"), "w+", encoding='utf-8') as outf:
- outf.write(u"jinja\n")
-
- # Copy assets in bootstrap/bootstrap3
- if child == 'bootstrap3-jinja':
- shutil.rmtree(os.path.join(out_theme, "assets"))
+ # Copy assets in bootstrap/bootstrap4
+ if child == 'bootstrap4-jinja':
+ assets_dir = os.path.join(out_theme, "assets")
+ if os.path.exists(assets_dir):
+ shutil.rmtree(assets_dir)
shutil.copytree(
os.path.join(in_theme, "assets"), os.path.join(out_theme, "assets"),
symlinks=True)
@@ -101,7 +102,7 @@ def jinjify(in_theme, out_theme):
def error(msg):
- print(colorama.Fore.RED + "ERROR:" + msg)
+ print("\033[1;31mERROR: {0}\033[0m".format(msg))
def mako2jinja(input_file):
@@ -110,7 +111,7 @@ def mako2jinja(input_file):
# TODO: OMG, this code is so horrible. Look at it; just look at it:
- macro_start = re.compile(r'(.*)<%.*def name="(.*?)".*>(.*)', re.IGNORECASE)
+ macro_start = re.compile(r'(.*)<%\s*def name="([^"]*?)"\s*>(.*)', re.IGNORECASE)
macro_end = re.compile(r'(.*)</%def>(.*)', re.IGNORECASE)
if_start = re.compile(r'(.*)% *if (.*):(.*)', re.IGNORECASE)
@@ -160,6 +161,14 @@ def mako2jinja(input_file):
if m_func_len:
line = func_len.sub(r'\1|length', line)
+ # Macro start/end
+ m_macro_start = macro_start.search(line)
+ if m_macro_start:
+ line = m_macro_start.expand(r'\1{% macro \2 %}\3') + '\n'
+ m_macro_end = macro_end.search(line)
+ if m_macro_end:
+ line = m_macro_end.expand(r'\1{% endmacro %}\2') + '\n'
+
# Process line for single 'whole line' replacements
m_macro_start = macro_start.search(line)
m_macro_end = macro_end.search(line)
@@ -180,11 +189,6 @@ def mako2jinja(input_file):
if m_comment_single_line:
output += m_comment_single_line.expand(r'{# \1 #}') + '\n'
- elif m_macro_start:
- output += m_macro_start.expand(r'\1{% macro \2 %}\3') + '\n'
- elif m_macro_end:
- output += m_macro_end.expand(r'\1{% endmacro %}\1') + '\n'
-
elif m_if_start:
output += m_if_start.expand(r'\1{% if \2 %}\3') + '\n'
elif m_if_else:
@@ -228,7 +232,7 @@ def jinjify_shortcodes(in_dir, out_dir):
data = mako2jinja(inf)
with open(out_file, 'w') as outf:
outf.write(data)
-
+
def usage():
print("Usage: python {} [in-dir] [out-dir]".format(sys.argv[0]))
@@ -240,7 +244,8 @@ if __name__ == "__main__":
print('Performing standard conversions:')
for m, j in (
('nikola/data/themes/base', 'nikola/data/themes/base-jinja'),
- ('nikola/data/themes/bootstrap3', 'nikola/data/themes/bootstrap3-jinja')
+ ('nikola/data/themes/bootstrap4', 'nikola/data/themes/bootstrap4-jinja'),
+ ('nikola/data/themes/bootblog4', 'nikola/data/themes/bootblog4-jinja'),
):
print(' {0} -> {1}'.format(m, j))
jinjify(m, j)
diff --git a/scripts/langmatrix.py b/scripts/langmatrix.py
new file mode 100755
index 0000000..357ddea
--- /dev/null
+++ b/scripts/langmatrix.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+"""A matrix! Of languages!"""
+import nikola.nikola
+
+keys = ['LUXON_LOCALES', 'MOMENTJS_LOCALES', 'PYPHEN_LOCALES', 'DOCUTILS_LOCALES']
+keys_short = ['language', 'luxon', 'moment', 'pyphen', 'docutils']
+print('\t'.join(keys_short))
+
+for tr in nikola.nikola.LEGAL_VALUES['TRANSLATIONS']:
+ if isinstance(tr, tuple):
+ tr = tr[0]
+ out = tr
+ if len(out) < 8:
+ out += '\t'
+ for key in keys:
+ out += '\t'
+ out += '\x1b[37;42;1m+' if tr in nikola.nikola.LEGAL_VALUES[key] else '\x1b[37;41;1m-'
+ print(out + '\t\x1b[0m')
+
+print('\t'.join(keys_short))
diff --git a/scripts/release b/scripts/release
index ce7b49e..e7b1aaa 100755
--- a/scripts/release
+++ b/scripts/release
@@ -1,7 +1,7 @@
-#!/bin/zsh
+#!/usr/bin/env zsh
# The Release Script
# Based on Python Project Template by Chris Warrick
-# Copyright © 2013-2016, Chris Warrick.
+# Copyright © 2013-2020, Chris Warrick.
# All rights reserved.
# Permission is hereby granted, free of charge, to any
@@ -86,12 +86,10 @@ status 'Replacing versions in files...'
sed "s/version=.*/version='$version',/g" setup.py -i
sed "s/version = .*/version = '$version'/g" docs/sphinx/conf.py -i
sed "s/release = .*/release = '$version'/g" docs/sphinx/conf.py -i
-sed "s/:Version: .*/:Version: $version/g" docs/*.txt docs/*.rst -i
+sed "s/:Version: .*/:Version: $version/g" docs/*.rst -i
sed "s/:Version: .*/:Version: Nikola $version/g" docs/man/nikola.rst -i
sed "s/__version__ = .*/__version__ = '$version'/g" nikola/__init__.py -i
-sed "s/version: .*/version: $version/g" snapcraft/stable/snapcraft.yaml -i
-sed "s/source-tag: .*/source-tag: v$version/g" snapcraft/stable/snapcraft.yaml -i
-sed "s/Nikola==.*'/Nikola==$version'/g" snapcraft/nikola.py -i
+sed "s/version: .*/version: $version/g" snapcraft.yaml -i
setopt errexit # Exit on errors
@@ -103,18 +101,18 @@ rst2man.py docs/man/nikola.rst > docs/man/nikola.1
gzip -f docs/man/nikola.1
status 'Updating path handler documentation...'
-scripts/document_path_handlers.py > docs/path_handlers.txt
+scripts/document_path_handlers.py > docs/path_handlers.rst
status 'Updating Jinja2 templates...'
scripts/jinjify.py
-status 'Updating Bower...'
-scripts/update-bower.sh
+status 'Updating npm assets...'
+scripts/update-npm-assets.sh
status 'Updating symlinked list...'
scripts/generate_symlinked_list.sh
status 'Updating website (conf.py and docs)...'
-scripts/generate_conf.py > ../nikola-site/listings/conf.py
+python -m nikola default_config > ../nikola-site/listings/conf.py
cp AUTHORS.txt CHANGES.txt ../nikola-site/stories
cp docs/*.* ../nikola-site/stories
-sed 's/Nikola v[0-9\.A_Za-z-]\{1,\}/Nikola'" v$version/g" ../nikola-site/stories/conf.txt -i
+sed 's/Nikola v[0-9\.A_Za-z-]\{1,\}/Nikola'" v$version/g" ../nikola-site/stories/conf.rst -i
status 'Generating locales...'
scripts/import_po.py
status 'List of locales:'
@@ -144,7 +142,7 @@ cleanup
git add -A --ignore-errors . || exit $?
-git commit -S -asm "Version $version" || exit $?
+git commit -S -am "Version $version" || exit $?
git tag -sm "Version $version" v$version || exit $?
git push --follow-tags origin master || exit $?
@@ -156,4 +154,3 @@ echo " * Create a GitHub release"
echo " * Update GitHub Issues milestones"
echo " * Update Nikola’s website"
echo " * Send out announcement e-mails"
-echo " * Update AUR PKGBUILDs"
diff --git a/scripts/snapcraft/nikola.sh b/scripts/snapcraft/nikola.sh
new file mode 100755
index 0000000..34e04bb
--- /dev/null
+++ b/scripts/snapcraft/nikola.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+export HOME=$SNAP_USER_DATA
+
+export I18NPATH=$SNAP/usr/share/i18n
+export LOCPATH=$SNAP_USER_DATA
+
+APPLANG=en_US
+APPENC=UTF-8
+APPLOC="$APPLANG.$APPENC"
+
+# generate a locale so we get properly working charsets and graphics
+if [ ! -e $SNAP_USER_DATA/$APPLOC ]; then
+ localedef --prefix=$SNAP_USER_DATA -f $APPENC -i $APPLANG $SNAP_USER_DATA/$APPLOC
+fi
+
+export LC_ALL=$APPLOC
+export LANG=$APPLOC
+export LANGUAGE=${APPLANG%_*}
+
+$SNAP/bin/nikola "$@"
diff --git a/scripts/update-bower.sh b/scripts/update-bower.sh
deleted file mode 100755
index fbf69a0..0000000
--- a/scripts/update-bower.sh
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/bin/bash
-
-# Update all bower packages
-bower update
-
-# Link bootstrap3 theme to bootstrap
-pushd nikola/data/themes/bootstrap3/assets/js/
-ln -sf ../../../../../../bower_components/bootstrap/dist/js/*js .
-rm npm.js
-git add .
-popd
-
-pushd nikola/data/themes/bootstrap3/assets/css/
-ln -sf ../../../../../../bower_components/bootstrap/dist/css/* .
-git add .
-popd
-
-pushd nikola/data/themes/bootstrap3/assets/fonts/
-ln -sf ../../../../../../bower_components/bootstrap/dist/fonts/* .
-git add .
-popd
-
-# Link moment.js to base theme
-pushd nikola/data/themes/base/assets/js
-ln -sf ../../../../../../bower_components/moment/min/moment-with-locales.min.js .
-git add moment-with-locales.min.js
-popd
-
-# Link jQuery to bootstrap theme
-pushd nikola/data/themes/bootstrap3/assets/js
-ln -sf ../../../../../../bower_components/jquery/dist/* .
-git add .
-popd
-
-
-# Link colorbox into bootstrap theme
-pushd nikola/data/themes/bootstrap3/assets/js
-ln -sf ../../../../../../bower_components/jquery-colorbox/jquery.colorbox.js .
-git add jquery.colorbox.js
-popd
-
-pushd nikola/data/themes/bootstrap3/assets/js/colorbox-i18n
-ln -sf ../../../../../../../bower_components/jquery-colorbox/i18n/* .
-git add .
-popd
-
-pushd nikola/data/themes/bootstrap3/assets/css/
-ln -sf ../../../../../../bower_components/jquery-colorbox/example3/colorbox.css .
-git add colorbox.css
-popd
-
-pushd nikola/data/themes/bootstrap3/assets/css/images/
-ln -sf ../../../../../../../bower_components/jquery-colorbox/example3/images/* .
-git add .
-popd
-
-scripts/generate_symlinked_list.sh
diff --git a/scripts/update-npm-assets.sh b/scripts/update-npm-assets.sh
new file mode 100755
index 0000000..b7da0a5
--- /dev/null
+++ b/scripts/update-npm-assets.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+
+# Update all npm packages
+cd npm_assets
+npm update
+cd ..
+
+# Link bootstrap assets to bootstrap
+pushd nikola/data/themes/bootstrap4/assets/js
+ln -sf ../../../../../../npm_assets/node_modules/bootstrap/dist/js/bootstrap.min.js .
+git add .
+popd
+
+pushd nikola/data/themes/bootstrap4/assets/css
+ln -sf ../../../../../../npm_assets/node_modules/bootstrap/dist/css/bootstrap.min.css .
+git add .
+popd
+
+# Link baguettebox.js to base theme
+pushd nikola/data/themes/base/assets/js
+ln -sf ../../../../../../npm_assets/node_modules/baguettebox.js/dist/baguetteBox.min.js .
+git add .
+popd
+pushd nikola/data/themes/base/assets/css
+ln -sf ../../../../../../npm_assets/node_modules/baguettebox.js/dist/baguetteBox.min.css .
+git add .
+popd
+
+# Link luxon and html5shiv to base theme
+pushd nikola/data/themes/base/assets/js
+ln -sf ../../../../../../npm_assets/node_modules/luxon/build/global/luxon.min.js .
+ln -sf ../../../../../../npm_assets/node_modules/html5shiv/dist/html5shiv-printshiv.min.js .
+ln -sf ../../../../../../npm_assets/node_modules/html5shiv/dist/html5shiv-printshiv.min.js html5.js
+git add moment-with-locales.min.js html5.js html5shiv-printshiv.min.js
+popd
+
+# Link jQuery to bootstrap theme
+pushd nikola/data/themes/bootstrap4/assets/js
+ln -sf ../../../../../../npm_assets/node_modules/jquery/dist/jquery.min.js .
+git add .
+popd
+
+# Link Popper.js to bootstrap theme
+pushd nikola/data/themes/bootstrap4/assets/js
+ln -sf ../../../../../../npm_assets/node_modules/popper.js/dist/umd/popper.min.js .
+git add .
+popd
+
+
+pushd nikola/plugins/command/auto
+ln -sf ../../../../npm_assets/node_modules/livereload-js/dist/livereload.js .
+popd
+
+scripts/generate_symlinked_list.sh
+
+# Verify baguetteBox patch
+grep PATCHED npm_assets/node_modules/baguettebox.js/dist/baguetteBox.js > /dev/null || printf '%b' '\033[1;31mWARNING: baguetteBox must be manually patched (in both unminified and minified versions), see npm_assets/baguetteBox-links-with-images-only.patch\033[0m\n'
+
+# vim:tw=0