From 5c2a279eb696fd62c31d4a65d900666dee6a639b Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Fri, 19 Dec 2025 21:56:12 -0500
Subject: [PATCH 042/181] Move payment form outside conditionals so it always
shows
---
remarkbox/templates/setup-namespace.j2 | 45 +++++++++++---------------
1 file changed, 19 insertions(+), 26 deletions(-)
diff --git a/remarkbox/templates/setup-namespace.j2 b/remarkbox/templates/setup-namespace.j2
index 26bf7d7..1aa80d4 100644
--- a/remarkbox/templates/setup-namespace.j2
+++ b/remarkbox/templates/setup-namespace.j2
@@ -98,19 +98,31 @@ Remarkbox will verify that you have posession of the domain on first load.">
-
-
+
+
+
+You should contact me, don't be to shy to say hi! I love hearing from all my friends, old and new! What are you up to? Any cool projects? Got an idea?
-Our business model depends on reciprocity and trust to succeed. We do not set price, but wish for contributions to maintain the product and service.
+
+
+russell.ballestrini@gmail.com
+
+
+
+{% endif %}
+{% endif %}
-
-
-There's no obligation to pay anything. : )
-
+
+
+
+
Our business model depends on reciprocity and trust to succeed. We do not set price, but wish for contributions to maintain the product and service.
-You should contact me, don't be to shy to say hi! I love hearing from all my friends, old and new! What are you up to? Any cool projects? Got an idea?
-
-
-
-russell.ballestrini@gmail.com
-
-
-
-{% endif %}
-{% endif %}
-
From 952120e5c5be134a55a2947834cdd289e38b9aca Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Fri, 19 Dec 2025 22:00:44 -0500
Subject: [PATCH 043/181] Fix test to match new billing page text
---
remarkbox/tests/test_views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/remarkbox/tests/test_views.py b/remarkbox/tests/test_views.py
index 5c1524a..f1101cb 100644
--- a/remarkbox/tests/test_views.py
+++ b/remarkbox/tests/test_views.py
@@ -409,7 +409,7 @@ class AuthenticatedFunctionalTests(FunctionalTests):
"""Test that the billing page loads for authenticated users."""
self._log_in_test_user(self.test_creds1)
res = self.testapp.get("/billing", status=200)
- self.assertIn(b"Make a Payment", res.body)
+ self.assertIn(b"Support Remarkbox", res.body)
self.assertIn(b"Pay with Stripe", res.body)
@patch("remarkbox.stripe.checkout.stripe.checkout.Session.create")
From 89abc7e7af81bd04f5102c3ef7755a5344416087 Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Fri, 19 Dec 2025 22:05:23 -0500
Subject: [PATCH 044/181] Move Step 3 payment form back into proper position
---
remarkbox/templates/setup-namespace.j2 | 40 +++++++++++++-------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/remarkbox/templates/setup-namespace.j2 b/remarkbox/templates/setup-namespace.j2
index 1aa80d4..fe4859d 100644
--- a/remarkbox/templates/setup-namespace.j2
+++ b/remarkbox/templates/setup-namespace.j2
@@ -98,26 +98,8 @@ Remarkbox will verify that you have posession of the domain on first load.">
-
-
-
-You should contact me, don't be to shy to say hi! I love hearing from all my friends, old and new! What are you up to? Any cool projects? Got an idea?
-
-
-
-russell.ballestrini@gmail.com
-
-
-
-{% endif %}
-{% endif %}
-
-
-
-
-
-
-
+
+
Our business model depends on reciprocity and trust to succeed. We do not set price, but wish for contributions to maintain the product and service.
+You should contact me, don't be to shy to say hi! I love hearing from all my friends, old and new! What are you up to? Any cool projects? Got an idea?
+
+
+
+russell.ballestrini@gmail.com
+
+
+
+{% endif %}
+{% endif %}
+
From e0fbd06e104315ddb7c752ee6061c72ef79490c4 Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Sat, 20 Dec 2025 06:26:53 -0500
Subject: [PATCH 045/181] Add CSS animation for node-children collapse/expand
---
remarkbox/static/css/common.css | 11 +++++++++++
remarkbox/static/js/custom.js | 13 +++++++++++++
2 files changed, 24 insertions(+)
diff --git a/remarkbox/static/css/common.css b/remarkbox/static/css/common.css
index 90c44f0..0a423a5 100644
--- a/remarkbox/static/css/common.css
+++ b/remarkbox/static/css/common.css
@@ -452,6 +452,17 @@ form.node-action {
}
}
+/* Node children collapse/expand animation */
+[id^="node-children-"] {
+ overflow: hidden;
+ max-height: 10000px;
+ transition: max-height 0.8s ease-out;
+}
+
+[id^="node-children-"].toggle-collapsed {
+ max-height: 0;
+}
+
.my-namespaces-div {
display: none;
position: absolute;
diff --git a/remarkbox/static/js/custom.js b/remarkbox/static/js/custom.js
index 67d4ef5..8003e6f 100644
--- a/remarkbox/static/js/custom.js
+++ b/remarkbox/static/js/custom.js
@@ -46,6 +46,19 @@ function toggle(target, button, off_text, on_text) {
if (typeof on_text === 'undefined') on_text = 'hide';
var el = document.getElementById(target);
var btn = document.getElementById(button);
+
+ // Handle node-children collapse (visible by default, toggle to hide)
+ if (target.indexOf('node-children-') === 0) {
+ if (el.classList.contains('toggle-collapsed')) {
+ el.classList.remove('toggle-collapsed');
+ btn.textContent = on_text;
+ } else {
+ el.classList.add('toggle-collapsed');
+ btn.textContent = off_text;
+ }
+ return;
+ }
+
if (el.classList.contains('toggle-open')) {
// Animate close, then update text
el.classList.add('toggle-closing');
From 58da72d344f5ee933546d20290db7954d1d0306d Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Sat, 20 Dec 2025 07:07:00 -0500
Subject: [PATCH 046/181] Fix overflow hidden cutting off avatars on mobile
---
remarkbox/static/css/common.css | 9 +++++----
remarkbox/static/js/custom.js | 11 +++++++++++
2 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/remarkbox/static/css/common.css b/remarkbox/static/css/common.css
index 0a423a5..013398f 100644
--- a/remarkbox/static/css/common.css
+++ b/remarkbox/static/css/common.css
@@ -453,14 +453,15 @@ form.node-action {
}
/* Node children collapse/expand animation */
-[id^="node-children-"] {
+[id^="node-children-"].toggle-collapsed {
overflow: hidden;
- max-height: 10000px;
+ max-height: 0;
transition: max-height 0.8s ease-out;
}
-[id^="node-children-"].toggle-collapsed {
- max-height: 0;
+[id^="node-children-"].toggle-expanding {
+ overflow: hidden;
+ transition: max-height 0.8s ease-out;
}
.my-namespaces-div {
diff --git a/remarkbox/static/js/custom.js b/remarkbox/static/js/custom.js
index 8003e6f..cf83576 100644
--- a/remarkbox/static/js/custom.js
+++ b/remarkbox/static/js/custom.js
@@ -50,10 +50,21 @@ function toggle(target, button, off_text, on_text) {
// Handle node-children collapse (visible by default, toggle to hide)
if (target.indexOf('node-children-') === 0) {
if (el.classList.contains('toggle-collapsed')) {
+ // Expanding: set max-height to scrollHeight, animate, then remove classes
+ el.classList.add('toggle-expanding');
+ el.style.maxHeight = el.scrollHeight + 'px';
el.classList.remove('toggle-collapsed');
btn.textContent = on_text;
+ setTimeout(function() {
+ el.classList.remove('toggle-expanding');
+ el.style.maxHeight = '';
+ }, 800);
} else {
+ // Collapsing: set max-height to current height, then collapse
+ el.style.maxHeight = el.scrollHeight + 'px';
+ el.offsetHeight; // force reflow
el.classList.add('toggle-collapsed');
+ el.style.maxHeight = '';
btn.textContent = off_text;
}
return;
From 19415fff06e01e5da3cc0cdf48537e54e341d79c Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Sat, 20 Dec 2025 10:50:52 -0500
Subject: [PATCH 047/181] Add allow_anonymous namespace setting for email-free
commenting
When enabled, commenters can post with just a display name (no email required).
Comments are attached to UserSurrogate instead of User.
Trade-offs for anonymous commenters:
- No email notifications for replies
- Cannot edit their comments
- Cannot log in to manage comments
- No cross-site identity
Works with existing moderation (hide_unless_approved) - anonymous
comments are never auto-approved when moderation is enabled.
---
remarkbox/models/namespace.py | 3 +
...add_allow_anonymous_column_to_namespace.py | 24 +++++
remarkbox/templates/namespace-settings.j2 | 5 ++
remarkbox/templates/snippets/email.j2 | 20 ++++-
.../views/authenticated/authenticated.py | 13 +++
remarkbox/views/new_thread.py | 88 +++++++++++--------
remarkbox/views/reply_node.py | 53 ++++++++---
7 files changed, 153 insertions(+), 53 deletions(-)
create mode 100644 remarkbox/scripts/alembic/versions/108519de76ac_add_allow_anonymous_column_to_namespace.py
diff --git a/remarkbox/models/namespace.py b/remarkbox/models/namespace.py
index 976f343..5c711d7 100644
--- a/remarkbox/models/namespace.py
+++ b/remarkbox/models/namespace.py
@@ -52,6 +52,7 @@ PROTECTED_ATTRIBUTES = {
"google_analytics_id": None,
"hide_unverified": False,
"hide_unless_approved": False,
+ "allow_anonymous": False,
"hide_powered_by": False,
"mathjax": False,
"link_protection": False,
@@ -92,6 +93,8 @@ class Namespace(RBase, Base):
hide_unverified = Column(Boolean, default=False)
# should a node be hidden until approved by a moderator?
hide_unless_approved = Column(Boolean, default=False)
+ # allow anonymous commenting (name only, no email required)
+ allow_anonymous = Column(Boolean, default=False)
# should we hide the poweredby Remarkbox logo?
hide_powered_by = Column(Boolean, default=False)
# should the list of root nodes in this namespace be public or hidden?
diff --git a/remarkbox/scripts/alembic/versions/108519de76ac_add_allow_anonymous_column_to_namespace.py b/remarkbox/scripts/alembic/versions/108519de76ac_add_allow_anonymous_column_to_namespace.py
new file mode 100644
index 0000000..d24feab
--- /dev/null
+++ b/remarkbox/scripts/alembic/versions/108519de76ac_add_allow_anonymous_column_to_namespace.py
@@ -0,0 +1,24 @@
+"""Add allow_anonymous column to namespace
+
+Revision ID: 108519de76ac
+Revises: 5188e62d0afb
+Create Date: 2025-12-20 11:05:36.134829
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '108519de76ac'
+down_revision = '5188e62d0afb'
+branch_labels = None
+depends_on = None
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.add_column('rb_namespace', sa.Column('allow_anonymous', sa.Boolean(), nullable=True, server_default='0'))
+
+
+def downgrade():
+ op.drop_column('rb_namespace', 'allow_anonymous')
diff --git a/remarkbox/templates/namespace-settings.j2 b/remarkbox/templates/namespace-settings.j2
index 5645cf0..c4e1234 100644
--- a/remarkbox/templates/namespace-settings.j2
+++ b/remarkbox/templates/namespace-settings.j2
@@ -39,6 +39,11 @@
If checked, hide new comments until approved by a moderator.
+
+
+If checked, allow comments without email verification. Commenters enter a display name only. They cannot edit comments or receive reply notifications.
+
+
If checked, replace all links in comments with [link removed].
diff --git a/remarkbox/templates/snippets/email.j2 b/remarkbox/templates/snippets/email.j2
index 8b1cc1a..c6d1c6a 100644
--- a/remarkbox/templates/snippets/email.j2
+++ b/remarkbox/templates/snippets/email.j2
@@ -1,4 +1,16 @@
{% if not request.user.authenticated %}
+ {% if request.namespace.allow_anonymous %}
+ {# Anonymous mode: show name field instead of email #}
+
+ {% else %}
+ {# Normal mode: require email #}
- {% if show_whats_next %}
+ {% if show_whats_next %}
What's next? check your email to log in!
- {% endif %}
+ {% endif %}
- {% if show_whats_next_notifications %}
+ {% if show_whats_next_notifications %}
What's next? verify your email address for reply notifications!
+ {% endif %}
{% endif %}
-
{% endif %}
diff --git a/remarkbox/views/authenticated/authenticated.py b/remarkbox/views/authenticated/authenticated.py
index 39067e4..1685612 100644
--- a/remarkbox/views/authenticated/authenticated.py
+++ b/remarkbox/views/authenticated/authenticated.py
@@ -60,6 +60,7 @@ def namespace_settings(request):
)
hide_unless_approved_checkbox = p.get("hide-unless-approved-checkbox", "off")
+ allow_anonymous_checkbox = p.get("allow-anonymous-checkbox", "off")
link_protection_checkbox = p.get("link-protection-checkbox", "off")
reverse_order_checkbox = p.get("reverse-order-checkbox", "off")
group_conversations_checkbox = p.get("group-conversations-checkbox", "off")
@@ -68,6 +69,7 @@ def namespace_settings(request):
hide_powered_by_checkbox = p.get("hide-powered-by-checkbox", "off")
hide_unless_approved = checkbox_to_bool(hide_unless_approved_checkbox)
+ allow_anonymous = checkbox_to_bool(allow_anonymous_checkbox)
link_protection = checkbox_to_bool(link_protection_checkbox)
reverse_order = checkbox_to_bool(reverse_order_checkbox)
group_conversations = checkbox_to_bool(group_conversations_checkbox)
@@ -146,6 +148,17 @@ def namespace_settings(request):
)
)
+ if allow_anonymous != request.namespace.allow_anonymous:
+ request.namespace.allow_anonymous = allow_anonymous
+ request.session.flash(
+ (
+ "You turned {} allow_anonymous".format(
+ allow_anonymous_checkbox
+ ),
+ "success",
+ )
+ )
+
if link_protection != request.namespace.link_protection:
request.namespace.link_protection = link_protection
request.session.flash(
diff --git a/remarkbox/views/new_thread.py b/remarkbox/views/new_thread.py
index d7c9a2c..315b6c9 100644
--- a/remarkbox/views/new_thread.py
+++ b/remarkbox/views/new_thread.py
@@ -4,7 +4,7 @@ from pyramid.csrf import check_csrf_token
from pyramid.httpexceptions import HTTPFound
-from remarkbox.models import create_root_node
+from remarkbox.models import create_root_node, get_or_create_user_surrogate_by_name
from . import get_referer_or_home, get_node_route_uri, set_node_to_pending_in_session
@@ -21,6 +21,7 @@ def new_thread(request):
"""Display new page and handle posting of form."""
thread_title = request.params.get("thread_title", "")
thread_data = request.params.get("thread_data", "")
+ anonymous_name = request.params.get("anonymous_name", "").strip()
if request.spam:
return request.spam
@@ -32,59 +33,76 @@ def new_thread(request):
if thread_title and thread_data:
# handle the submitted form new/create form.
- if request.user is None:
+ # Handle anonymous mode vs regular mode
+ user_surrogate = None
+ if request.namespace.allow_anonymous and not request.user:
+ # Anonymous mode: create or get a surrogate
+ if not anonymous_name:
+ anonymous_name = "Anonymous"
+ user_surrogate = get_or_create_user_surrogate_by_name(
+ request.dbsession, anonymous_name, request.namespace
+ )
+ elif request.user is None:
+ # Regular mode: require email/user
request.session.flash(
("Press the back button to fix your email address", "error")
)
return HTTPFound(get_referer_or_home(request))
- else:
- # create a new root node.
- node = create_root_node()
+ # create a new root node.
+ node = create_root_node()
+ node.namespace = request.namespace
+ node.ip_address = unicode(request.client_addr)
+ node.title = thread_title
+ node.set_data(thread_data)
+ # Handle anonymous vs authenticated user
+ if user_surrogate:
+ # Anonymous mode: attach surrogate, mark as verified
+ node.user_surrogate = user_surrogate
+ node.verified = True
+ node_event = None # No notifications for anonymous posts
+ request.dbsession.add(user_surrogate)
+ else:
+ # Normal mode: attach user
node.user = request.user
node.verified = request.user.authenticated
- node.namespace = request.namespace
- node.ip_address = unicode(request.client_addr)
-
- node.title = thread_title
- node.set_data(thread_data)
-
node_event = node.new_event(request.user, "created")
-
- request.dbsession.add(node)
- request.dbsession.add(node_event)
request.dbsession.add(request.user)
- request.dbsession.add(node.namespace)
- request.dbsession.flush()
+ request.dbsession.add(node)
+ if node_event:
+ request.dbsession.add(node_event)
+ request.dbsession.add(node.namespace)
+ request.dbsession.flush()
+
+ if node_event:
# TODO: schedule_notification expects the request to have a node.
request.node = node
-
schedule_notifications(request, node_event)
- msg = ("Your post was successful!", "success")
- request.session.flash(msg)
+ msg = ("Your post was successful!", "success")
+ request.session.flash(msg)
- # set return_to to the node's URI.
- return_to = get_node_route_uri(request, node)
+ # set return_to to the node's URI.
+ return_to = get_node_route_uri(request, node)
- if node.verified == True:
- # Redirect to new node if verified.
- return HTTPFound(return_to)
+ # Anonymous users are always verified, redirect immediately
+ if user_surrogate or node.verified:
+ return HTTPFound(return_to)
- set_node_to_pending_in_session(request, node)
+ set_node_to_pending_in_session(request, node)
- # Redirect to join-or-log-in, posting email and submit.
- uri = request.route_url(
- route_name="basic-join-or-log-in",
- _query={
- "email": request.user.email,
- "return-to": return_to,
- "submit": True,
- },
- )
- return HTTPFound(uri)
+ # Redirect to join-or-log-in, posting email and submit.
+ uri = request.route_url(
+ route_name="basic-join-or-log-in",
+ _query={
+ "email": request.user.email,
+ "return-to": return_to,
+ "submit": True,
+ },
+ )
+ return HTTPFound(uri)
return {
"title": "Create a new thread",
diff --git a/remarkbox/views/reply_node.py b/remarkbox/views/reply_node.py
index 80767d1..9fba08f 100644
--- a/remarkbox/views/reply_node.py
+++ b/remarkbox/views/reply_node.py
@@ -12,6 +12,7 @@ from . import (
)
from remarkbox.lib.notify import schedule_notifications
+from remarkbox.models import get_or_create_user_surrogate_by_name
try:
unicode("")
@@ -27,6 +28,7 @@ except:
def reply_node(request):
"""handle posting of reply form from show-node pages."""
thread_data = request.params.get("thread_data", "")
+ anonymous_name = request.params.get("anonymous_name", "").strip()
# return early if spam attribute is truthy.
if request.spam:
@@ -43,8 +45,17 @@ def reply_node(request):
request.session.flash(("No remarks for the disabled.", "error"))
return HTTPFound(get_referer_or_home(request))
- # flash error and return early if user is None.
- if request.user is None:
+ # Handle anonymous mode vs regular mode
+ user_surrogate = None
+ if request.namespace.allow_anonymous and not request.user:
+ # Anonymous mode: create or get a surrogate
+ if not anonymous_name:
+ anonymous_name = "Anonymous"
+ user_surrogate = get_or_create_user_surrogate_by_name(
+ request.dbsession, anonymous_name, request.namespace
+ )
+ elif request.user is None:
+ # Regular mode: require email/user
request.session.flash(
("Press the back button to fix your email address", "error")
)
@@ -72,16 +83,30 @@ def reply_node(request):
# STEP 2: attach a brand new child node to parent node.
child = parent.new_child()
- child.user = request.user
child.ip_address = unicode(request.client_addr)
- child.verified = request.user.authenticated
child.set_data(thread_data, namespace=request.namespace)
- child_event = child.new_event(request.user, "commented")
+
+ # Handle anonymous vs authenticated user
+ if user_surrogate:
+ # Anonymous mode: attach surrogate, mark as verified (no email to verify)
+ child.user_surrogate = user_surrogate
+ child.verified = True
+ child_event = None # No notifications for anonymous comments
+ request.dbsession.add(user_surrogate)
+ else:
+ # Normal mode: attach user
+ child.user = request.user
+ child.verified = request.user.authenticated
+ child_event = child.new_event(request.user, "commented")
if request.namespace.hide_unless_approved:
# by default comments are approved, unless Namespace hide_unless_approved
# is enabled, moderators nodes are always auto approved.
- child.approved = request.namespace.is_moderator(request.user)
+ if request.user:
+ child.approved = request.namespace.is_moderator(request.user)
+ else:
+ # Anonymous users are never auto-approved when moderation is on
+ child.approved = False
# STEP 3: update root's changed timestamp.
# TODO: maybe we should find a better way to "bump" a thread.
@@ -90,29 +115,29 @@ def reply_node(request):
parent._invalidate_cache()
# STEP 4: commit to database.
- request.dbsession.add(request.user)
+ if request.user:
+ request.dbsession.add(request.user)
request.dbsession.add(child)
- request.dbsession.add(child_event)
+ if child_event:
+ request.dbsession.add(child_event)
request.dbsession.add(parent)
request.dbsession.add(parent.root)
request.dbsession.flush()
- schedule_notifications(request, child_event)
+ if child_event:
+ schedule_notifications(request, child_event)
msg = ("Your post was successful!", "success")
request.session.flash(msg)
- ### TODO: everything below this is pretty much crap code...
- # and likely deserves a flowchart...
-
# set return_to URI.
if request.mode == "embed":
return_to = get_embed_route_uri(request, child.root.uri.data, child.id)
else:
return_to = get_node_route_uri(request, child.root, child.id)
- if child.verified == True:
- # Redirect to new node if user and new node is verified.
+ # Anonymous users are always verified, redirect immediately
+ if user_surrogate or child.verified:
return HTTPFound(return_to)
set_node_to_pending_in_session(request, child)
From 6b2be261f5a15efab65886f05435fa92d9943dd2 Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Sat, 20 Dec 2025 11:17:12 -0500
Subject: [PATCH 048/181] Add functional tests for anonymous commenting feature
Tests cover:
- Anonymous reply creates UserSurrogate
- Anonymous reply with no name defaults to 'Anonymous'
- Regular namespace still requires email
- Anonymous comments are marked as verified
- Namespace settings toggle for allow_anonymous
---
remarkbox/tests/test_views.py | 269 ++++++++++++++++++++++++++++++++++
1 file changed, 269 insertions(+)
diff --git a/remarkbox/tests/test_views.py b/remarkbox/tests/test_views.py
index f1101cb..a34f4ce 100644
--- a/remarkbox/tests/test_views.py
+++ b/remarkbox/tests/test_views.py
@@ -7,7 +7,9 @@ from remarkbox.models import (
get_tm_session,
get_or_create_user_by_email,
get_user_by_email,
+ get_or_create_namespace,
NodeEventNotification,
+ UserSurrogate,
)
from remarkbox.models.meta import Base
@@ -477,3 +479,270 @@ class AuthenticatedFunctionalTests(FunctionalTests):
redirect_res = self.testapp.get("/billing/success", status=302)
res = redirect_res.follow()
self.assertIn(b"Missing session information", res.body)
+
+
+class AnonymousCommentingFunctionalTests(FunctionalTests):
+ """Tests for anonymous commenting feature."""
+
+ @classmethod
+ def setUpClass(cls):
+ try:
+ FunctionalTests.setUpClass.im_func(cls)
+ except AttributeError:
+ FunctionalTests.setUpClass.__func__(cls)
+
+ def setUp(self):
+ # Create a namespace with allow_anonymous enabled
+ anon_ns = get_or_create_namespace(
+ self.dbsession, "anon-test.example.com"
+ )
+ anon_ns.allow_anonymous = True
+ self.dbsession.add(anon_ns)
+
+ # Create a namespace with allow_anonymous disabled (default)
+ regular_ns = get_or_create_namespace(
+ self.dbsession, "regular-test.example.com"
+ )
+ regular_ns.allow_anonymous = False
+ self.dbsession.add(regular_ns)
+
+ # Create a test user for namespace ownership
+ test_user = get_or_create_user_by_email(
+ self.dbsession, "anon-test@remarkbox.com"
+ )
+ self.raw_otp = test_user.new_password()
+ self.dbsession.add(test_user)
+
+ self.dbsession.flush()
+
+ # Store IDs and names before commit
+ self.anon_namespace_id = anon_ns.id
+ self.anon_namespace_name = str(anon_ns.name)
+ self.regular_namespace_id = regular_ns.id
+ self.regular_namespace_name = str(regular_ns.name)
+
+ self.tm.commit()
+
+ self.test_creds = ("anon-test@remarkbox.com", self.raw_otp)
+
+ def tearDown(self):
+ super(AnonymousCommentingFunctionalTests, self).tearDown()
+ # Clean up surrogates created during tests
+ self.dbsession.query(UserSurrogate).filter(
+ UserSurrogate.namespace_id.in_([
+ self.anon_namespace_id,
+ self.regular_namespace_id
+ ])
+ ).delete(synchronize_session=False)
+ # Requery user before delete
+ user = get_user_by_email(self.dbsession, "anon-test@remarkbox.com")
+ if user:
+ self.dbsession.delete(user)
+ self.dbsession.flush()
+ self.tm.commit()
+
+ def _log_in_test_user(self):
+ res_login = self.testapp.post(
+ "/verification-challenge?email={}&raw-otp={}&submit".format(*self.test_creds)
+ )
+ res_csrf = self.testapp.get("/")
+ self.csrf = res_csrf.form.fields["csrf_token"][0].value
+ return res_login
+
+ def test_anonymous_reply_creates_surrogate(self):
+ """Test that anonymous reply creates a UserSurrogate."""
+ # First create a thread with an authenticated user
+ self._log_in_test_user()
+
+ # Create a root node in the anonymous namespace
+ from remarkbox.models import create_root_node
+ anon_ns = get_or_create_namespace(self.dbsession, self.anon_namespace_name)
+ user = get_or_create_user_by_email(self.dbsession, "anon-test@remarkbox.com")
+
+ root = create_root_node()
+ root.namespace = anon_ns
+ root.user = user
+ root.verified = True
+ root.title = "Test Thread"
+ root.set_data("Test content")
+ self.dbsession.add(root)
+ self.dbsession.flush()
+ root_id = str(root.id)
+ self.tm.commit()
+
+ # Log out
+ self.testapp.get("/log-out")
+
+ # Post anonymous reply (no email, just name)
+ redirect_res = self.testapp.post(
+ "/{}/reply".format(root_id),
+ {
+ "thread_data": "Anonymous comment here",
+ "anonymous_name": "TestAnon",
+ },
+ status=302,
+ )
+
+ # Should redirect to the thread (not to login)
+ res = redirect_res.follow()
+ self.assertIn(b"Your post was successful!", res.body)
+
+ # Verify a surrogate was created
+ surrogate = self.dbsession.query(UserSurrogate).filter(
+ UserSurrogate.name == "TestAnon",
+ UserSurrogate.namespace_id == self.anon_namespace_id
+ ).first()
+ self.assertIsNotNone(surrogate)
+
+ def test_anonymous_reply_default_name(self):
+ """Test that anonymous reply without name uses 'Anonymous'."""
+ self._log_in_test_user()
+
+ from remarkbox.models import create_root_node
+ anon_ns = get_or_create_namespace(self.dbsession, self.anon_namespace_name)
+ user = get_or_create_user_by_email(self.dbsession, "anon-test@remarkbox.com")
+
+ root = create_root_node()
+ root.namespace = anon_ns
+ root.user = user
+ root.verified = True
+ root.title = "Test Thread 2"
+ root.set_data("Test content 2")
+ self.dbsession.add(root)
+ self.dbsession.flush()
+ root_id = str(root.id)
+ self.tm.commit()
+
+ self.testapp.get("/log-out")
+
+ # Post without anonymous_name
+ redirect_res = self.testapp.post(
+ "/{}/reply".format(root_id),
+ {
+ "thread_data": "Anonymous comment without name",
+ },
+ status=302,
+ )
+
+ res = redirect_res.follow()
+ self.assertIn(b"Your post was successful!", res.body)
+
+ # Verify surrogate with default name
+ surrogate = self.dbsession.query(UserSurrogate).filter(
+ UserSurrogate.name == "Anonymous",
+ UserSurrogate.namespace_id == self.anon_namespace_id
+ ).first()
+ self.assertIsNotNone(surrogate)
+
+ def test_regular_namespace_requires_email(self):
+ """Test that non-anonymous namespace still requires email."""
+ self._log_in_test_user()
+
+ from remarkbox.models import create_root_node
+ regular_ns = get_or_create_namespace(self.dbsession, self.regular_namespace_name)
+ user = get_or_create_user_by_email(self.dbsession, "anon-test@remarkbox.com")
+
+ root = create_root_node()
+ root.namespace = regular_ns
+ root.user = user
+ root.verified = True
+ root.title = "Regular Thread"
+ root.set_data("Regular content")
+ self.dbsession.add(root)
+ self.dbsession.flush()
+ root_id = str(root.id)
+ self.tm.commit()
+
+ self.testapp.get("/log-out")
+
+ # Try to post without email on regular namespace
+ redirect_res = self.testapp.post(
+ "/{}/reply".format(root_id),
+ {
+ "thread_data": "This should fail",
+ "anonymous_name": "ShouldFail",
+ },
+ status=302,
+ )
+
+ res = redirect_res.follow()
+ self.assertIn(b"Press the back button to fix your email address", res.body)
+
+ def test_anonymous_comment_is_verified(self):
+ """Test that anonymous comments are marked as verified."""
+ self._log_in_test_user()
+
+ from remarkbox.models import create_root_node
+ anon_ns = get_or_create_namespace(self.dbsession, self.anon_namespace_name)
+ user = get_or_create_user_by_email(self.dbsession, "anon-test@remarkbox.com")
+
+ root = create_root_node()
+ root.namespace = anon_ns
+ root.user = user
+ root.verified = True
+ root.title = "Verified Test Thread"
+ root.set_data("Verified test content")
+ self.dbsession.add(root)
+ self.dbsession.flush()
+ root_id = root.id
+ self.tm.commit()
+
+ self.testapp.get("/log-out")
+
+ self.testapp.post(
+ "/{}/reply".format(root_id),
+ {
+ "thread_data": "Anonymous verified comment",
+ "anonymous_name": "VerifiedAnon",
+ },
+ status=302,
+ )
+
+ # Check the node is verified
+ child = self.dbsession.query(Node).filter(
+ Node.parent_id == root_id
+ ).first()
+ self.assertIsNotNone(child)
+ self.assertTrue(child.verified)
+ self.assertIsNotNone(child.user_surrogate)
+ self.assertIsNone(child.user)
+
+ def test_namespace_settings_toggle(self):
+ """Test that namespace owner can toggle allow_anonymous setting."""
+ self._log_in_test_user()
+
+ # Make user owner of namespace
+ from remarkbox.models import Namespace
+ anon_ns = get_or_create_namespace(self.dbsession, self.anon_namespace_name)
+ user = get_or_create_user_by_email(self.dbsession, "anon-test@remarkbox.com")
+
+ anon_ns.set_role_for_user(user, "owner")
+ self.dbsession.add(anon_ns)
+ self.dbsession.flush()
+ self.tm.commit()
+
+ # Toggle off
+ self.testapp.post(
+ "/ns/{}/settings".format(self.anon_namespace_name),
+ {
+ "csrf_token": self.csrf,
+ # Not including allow-anonymous-checkbox means it's unchecked
+ },
+ )
+
+ ns = self.dbsession.query(Namespace).filter(
+ Namespace.id == self.anon_namespace_id
+ ).first()
+ self.assertFalse(ns.allow_anonymous)
+
+ # Toggle on
+ self.testapp.post(
+ "/ns/{}/settings".format(self.anon_namespace_name),
+ {
+ "csrf_token": self.csrf,
+ "allow-anonymous-checkbox": "on",
+ },
+ )
+
+ self.dbsession.expire(ns)
+ self.assertTrue(ns.allow_anonymous)
From 033fe1f1a779a82e3d971cac48dbd363ccfe9a16 Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Mon, 29 Dec 2025 14:26:10 +0000
Subject: [PATCH 049/181] Update setup.py
---
setup.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index e50b396..f65ea14 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@ with open(os.path.join(here, "README.rst"), "r", encoding="utf-8") as f:
setup(
name="remarkbox",
- version="1.0.4",
+ version="1.0.5",
description="remarkbox",
long_description=long_description,
author="Russell Ballestrini",
@@ -85,6 +85,7 @@ setup(
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
+ "Programming Language :: Python :: 3.15",
"Programming Language :: Python",
"Framework :: Pyramid",
"Topic :: Internet :: WWW/HTTP",
From a362190ea5efae79f88fdd0d8d7f670a30bafba6 Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Mon, 29 Dec 2025 14:29:28 +0000
Subject: [PATCH 050/181] Update .gitlab-ci.yml
---
.gitlab-ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a25ef46..54eb762 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -64,7 +64,7 @@ pypi-twine:
only:
- tags
script:
- - pip install --upgrade pip
+ #- pip install --upgrade pip
- pip install twine
- python3 setup.py sdist bdist_wheel
- twine upload dist/*
From 00edd84398a207f2f4c006cc8a431d0c3be5e9c3 Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Mon, 29 Dec 2025 14:31:53 +0000
Subject: [PATCH 051/181] Update .gitlab-ci.yml file
---
.gitlab-ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 54eb762..3b9247e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -65,6 +65,6 @@ pypi-twine:
- tags
script:
#- pip install --upgrade pip
- - pip install twine
+ #- pip install twine
- python3 setup.py sdist bdist_wheel
- twine upload dist/*
From 9936b21467c948fb7a4924de7fc9917bc47f8936 Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Mon, 29 Dec 2025 09:35:08 -0500
Subject: [PATCH 052/181] Add twine-venv target in /tmp for PyPI uploads
Uses a persistent virtualenv in /tmp/twine-venv to avoid
relying on system python/twine in CI.
---
.gitlab-ci.yml | 5 +----
Makefile | 19 +++++++++++++++++++
2 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3b9247e..f00d6d6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -64,7 +64,4 @@ pypi-twine:
only:
- tags
script:
- #- pip install --upgrade pip
- #- pip install twine
- - python3 setup.py sdist bdist_wheel
- - twine upload dist/*
+ - make twine-upload
diff --git a/Makefile b/Makefile
index 5ddd7f0..5708859 100644
--- a/Makefile
+++ b/Makefile
@@ -124,6 +124,25 @@ http: venv
@echo "Starting simple HTTP server on port 8000..."
$(PYTHON) -m http.server 8000
+# -----------------------------------------------------------------------------
+# Twine Upload Target (uses /tmp venv to avoid system python)
+# -----------------------------------------------------------------------------
+
+TWINE_VENV = /tmp/twine-venv
+TWINE = $(TWINE_VENV)/bin/twine
+
+$(TWINE_VENV)/bin/twine:
+ @echo "Creating twine virtualenv in $(TWINE_VENV)..."
+ python3 -m venv $(TWINE_VENV)
+ $(TWINE_VENV)/bin/pip install --upgrade pip twine
+
+twine-venv: $(TWINE_VENV)/bin/twine
+
+twine-upload: twine-venv
+ @echo "Building and uploading to PyPI..."
+ python3 setup.py sdist bdist_wheel
+ $(TWINE) upload dist/*
+
# -----------------------------------------------------------------------------
# Cleanup Target
# -----------------------------------------------------------------------------
From a6c04d565a93f524100fadde138519cff69f3fd5 Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Sun, 11 Jan 2026 09:11:07 -0500
Subject: [PATCH 053/181] Add google_site_verification support for namespaces
- Add google_site_verification column to rb_namespace table
- Add settings form field in Stand-alone Mode Settings
- Render meta tag on root path when set
- Update CLAUDE.md with Alembic migration workflow
- Fix development.ini alembic paths
---
CLAUDE.md | 10 ++++++++
development.ini | 4 ++--
remarkbox/models/namespace.py | 2 ++
...add_google_site_verification_column_to_.py | 24 +++++++++++++++++++
remarkbox/templates/base.j2 | 3 +++
remarkbox/templates/namespace-settings.j2 | 6 +++++
.../views/authenticated/authenticated.py | 14 +++++++++++
7 files changed, 61 insertions(+), 2 deletions(-)
create mode 100644 remarkbox/scripts/alembic/versions/d1213344ca99_add_google_site_verification_column_to_.py
diff --git a/CLAUDE.md b/CLAUDE.md
index 802d45e..36dea23 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -24,3 +24,13 @@ Commit message here.
- Any fake corporate entities as co-authors
**Only attribute real humans** as co-authors when collaborating.
+
+## Database Migrations (Alembic)
+
+When adding new columns or modifying the database schema:
+
+1. **Backup SQLite first**: `cp data/remarkbox.sqlite data/remarkbox.sqlite.bak`
+2. **Add the column to the model** in `remarkbox/models/`
+3. **Generate migration**: `alembic -c development.ini revision --autogenerate -m "description"`
+4. **Clean up migration**: Remove extra autogenerated changes, keep only the new field
+5. **Run migration**: `alembic -c development.ini upgrade head`
diff --git a/development.ini b/development.ini
index 3da0494..975c8cb 100644
--- a/development.ini
+++ b/development.ini
@@ -2,8 +2,8 @@
# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
[alembic]
-script_location = remarkbox:scripts/alembic
-sqlalchemy.url = sqlite:///%(here)s/remarkbox.sqlite
+script_location = remarkbox/scripts/alembic
+sqlalchemy.url = sqlite:///%(here)s/data/remarkbox.sqlite
[app:main]
use = egg:remarkbox
diff --git a/remarkbox/models/namespace.py b/remarkbox/models/namespace.py
index 5c711d7..63fe580 100644
--- a/remarkbox/models/namespace.py
+++ b/remarkbox/models/namespace.py
@@ -86,6 +86,8 @@ class Namespace(RBase, Base):
owner_request_timestamp = Column(BigInteger, nullable=True)
# optional google analytics id for stand alone mode.
google_analytics_id = Column(Unicode(18), nullable=True)
+ # optional google site verification code for stand alone mode.
+ google_site_verification = Column(Unicode(128), nullable=True)
# allow anyone to edit the root node. (not implemented yet)
wiki = Column(Boolean, default=False)
# by default we show all nodes but a Namespace can change this behavior.
diff --git a/remarkbox/scripts/alembic/versions/d1213344ca99_add_google_site_verification_column_to_.py b/remarkbox/scripts/alembic/versions/d1213344ca99_add_google_site_verification_column_to_.py
new file mode 100644
index 0000000..de4cab4
--- /dev/null
+++ b/remarkbox/scripts/alembic/versions/d1213344ca99_add_google_site_verification_column_to_.py
@@ -0,0 +1,24 @@
+"""add google_site_verification column to namespace
+
+Revision ID: d1213344ca99
+Revises: 108519de76ac
+Create Date: 2026-01-11 09:06:53.272415
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'd1213344ca99'
+down_revision = '108519de76ac'
+branch_labels = None
+depends_on = None
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.add_column('rb_namespace', sa.Column('google_site_verification', sa.Unicode(length=128), nullable=True))
+
+
+def downgrade():
+ op.drop_column('rb_namespace', 'google_site_verification')
diff --git a/remarkbox/templates/base.j2 b/remarkbox/templates/base.j2
index f0c7d78..5769f0c 100644
--- a/remarkbox/templates/base.j2
+++ b/remarkbox/templates/base.j2
@@ -9,6 +9,9 @@
+{% if request.path == '/' and request.namespace.google_site_verification %}
+
+{% endif %}
{% include 'snippets/stylesheet-includes.j2' %}
diff --git a/remarkbox/templates/namespace-settings.j2 b/remarkbox/templates/namespace-settings.j2
index c4e1234..2773df4 100644
--- a/remarkbox/templates/namespace-settings.j2
+++ b/remarkbox/templates/namespace-settings.j2
@@ -125,6 +125,12 @@ You can host your own CSS file and link it here.
+
+
+
+
+
+
{% set submit_button_classes = 'button-right green-button' %}
{% set submit_button_value = 'save changes' %}
{% include 'snippets/submit.j2' %}
diff --git a/remarkbox/views/authenticated/authenticated.py b/remarkbox/views/authenticated/authenticated.py
index 1685612..247f12f 100644
--- a/remarkbox/views/authenticated/authenticated.py
+++ b/remarkbox/views/authenticated/authenticated.py
@@ -58,6 +58,9 @@ def namespace_settings(request):
google_analytics_id = p.get(
"google-analytics-id", request.namespace.google_analytics_id
)
+ google_site_verification = p.get(
+ "google-site-verification", request.namespace.google_site_verification
+ )
hide_unless_approved_checkbox = p.get("hide-unless-approved-checkbox", "off")
allow_anonymous_checkbox = p.get("allow-anonymous-checkbox", "off")
@@ -137,6 +140,17 @@ def namespace_settings(request):
)
)
+ if google_site_verification != request.namespace.google_site_verification and (
+ google_site_verification or request.namespace.google_site_verification
+ ):
+ request.namespace.google_site_verification = google_site_verification
+ request.session.flash(
+ (
+ "Success, you changed the Google Site Verification code for stand alone mode.",
+ "success",
+ )
+ )
+
if hide_unless_approved != request.namespace.hide_unless_approved:
request.namespace.hide_unless_approved = hide_unless_approved
request.session.flash(
From 06ba1067954a3f107f15f1ad8f2059ee14fa32e2 Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Sun, 11 Jan 2026 09:28:59 -0500
Subject: [PATCH 054/181] Fix sitemap lastmod date format for Google
---
remarkbox/templates/sitemap.xml.j2 | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/remarkbox/templates/sitemap.xml.j2 b/remarkbox/templates/sitemap.xml.j2
index 99dfe39..4894900 100644
--- a/remarkbox/templates/sitemap.xml.j2
+++ b/remarkbox/templates/sitemap.xml.j2
@@ -5,7 +5,7 @@
{{ request.host_url }}{{ node.path }}weekly0.8
- {{ node.changed_datetime.isoformat() }}
+ {{ node.changed_datetime.strftime('%Y-%m-%d') }}
{% endfor %}
From a49446cfefb807b08ee3bceb3cb04c497967998a Mon Sep 17 00:00:00 2001
From: "russell@unturf.com"
Date: Wed, 21 Jan 2026 12:36:31 -0500
Subject: [PATCH 055/181] Add parallel test execution with pytest-xdist
Implements per-worker database isolation for parallel test runs:
- Add pytest-xdist and pytest-cov to test dependencies
- Configure Makefile to run tests with -n auto flag
- Update test.ini to use environment variable for database path
- Create conftest.py with per-worker database isolation
- Enable SQLite WAL mode for better concurrency
- Auto-cleanup test databases after completion
Expected performance improvement similar to make_post_sell (~16x speedup).
Co-Authored-By: Claude Sonnet 4.5
---
Makefile | 5 +--
remarkbox/tests/conftest.py | 77 +++++++++++++++++++++++++++++++++++++
requirements-test.txt | 2 +
test.ini | 4 +-
4 files changed, 83 insertions(+), 5 deletions(-)
create mode 100644 remarkbox/tests/conftest.py
diff --git a/Makefile b/Makefile
index 5708859..bd6a875 100644
--- a/Makefile
+++ b/Makefile
@@ -113,11 +113,10 @@ activate:
@echo "To activate the virtual environment, run:"
@echo " source $(VENV_DIR)/bin/activate"
-# Run the test suite (installs test dependencies if needed)
# Run the test suite (installs test dependencies if needed)
test: install-source-dev-and-test
- @echo "Running tests..."
- $(VENV_DIR)/bin/py.test
+ @echo "Running tests in parallel..."
+ $(VENV_DIR)/bin/py.test -n auto
# Start a simple HTTP server (for serving static files like index.html)
http: venv
diff --git a/remarkbox/tests/conftest.py b/remarkbox/tests/conftest.py
new file mode 100644
index 0000000..1c6e372
--- /dev/null
+++ b/remarkbox/tests/conftest.py
@@ -0,0 +1,77 @@
+"""
+Pytest configuration for parallel test execution.
+
+This module configures pytest-xdist to use isolated databases per worker
+to avoid SQLite locking issues during parallel test runs.
+"""
+import os
+import pytest
+
+
+def pytest_configure(config):
+ """
+ Configure test database isolation for parallel execution.
+
+ Each pytest-xdist worker gets its own database file to prevent
+ SQLite locking conflicts. WAL mode is enabled for better concurrency.
+ """
+ # Get worker ID (e.g., "gw0", "gw1", etc.) for pytest-xdist
+ worker_id = os.environ.get("PYTEST_XDIST_WORKER", "master")
+
+ # Set unique database path for this worker
+ test_db_path = f"test-remarkbox_{worker_id}.sqlite"
+ os.environ["TEST_DATABASE_PATH"] = test_db_path
+
+ # Store for cleanup
+ config.test_db_path = test_db_path
+
+
+def pytest_unconfigure(config):
+ """Clean up test database after all tests complete."""
+ if hasattr(config, "test_db_path"):
+ db_path = config.test_db_path
+ if os.path.exists(db_path):
+ try:
+ os.remove(db_path)
+ except Exception as e:
+ print(f"Warning: Could not remove test database {db_path}: {e}")
+
+
+@pytest.fixture(scope="session")
+def db_engine(request):
+ """
+ Create a SQLAlchemy engine with WAL mode enabled for concurrency.
+
+ WAL (Write-Ahead Logging) mode allows multiple readers while a writer
+ is active, improving parallel test performance.
+ """
+ from sqlalchemy import create_engine, event
+
+ # Use worker-specific database
+ worker_id = os.environ.get("PYTEST_XDIST_WORKER", "master")
+ db_path = f"test-remarkbox_{worker_id}.sqlite"
+ db_url = f"sqlite:///{db_path}"
+
+ engine = create_engine(
+ db_url,
+ echo=False,
+ # Important: Use NullPool to avoid connection sharing issues
+ poolclass=__import__("sqlalchemy.pool", fromlist=["NullPool"]).NullPool,
+ )
+
+ # Enable WAL mode for better concurrency
+ @event.listens_for(engine, "connect")
+ def set_sqlite_pragma(dbapi_conn, connection_record):
+ cursor = dbapi_conn.cursor()
+ # Enable WAL mode for concurrent access
+ cursor.execute("PRAGMA journal_mode=WAL")
+ # Increase cache size for better performance
+ cursor.execute("PRAGMA cache_size=-64000") # 64MB
+ # Enable foreign keys
+ cursor.execute("PRAGMA foreign_keys=ON")
+ cursor.close()
+
+ yield engine
+
+ # Cleanup
+ engine.dispose()
diff --git a/requirements-test.txt b/requirements-test.txt
index 0a36b85..c9b788e 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,5 +1,7 @@
# testing.
pytest
+pytest-cov
+pytest-xdist # Parallel test execution
nose
mock
webtest
diff --git a/test.ini b/test.ini
index bc805bf..a3781db 100644
--- a/test.ini
+++ b/test.ini
@@ -2,12 +2,12 @@
[alembic]
script_location = remarkbox:scripts/alembic
-sqlalchemy.url = sqlite:///%(here)s/test-remarkbox.sqlite
+sqlalchemy.url = sqlite:///%(here)s/${TEST_DATABASE_PATH:-test-remarkbox.sqlite}
[app:main]
use = egg:remarkbox
-sqlalchemy.url = sqlite:///%(here)s/test-remarkbox.sqlite
+sqlalchemy.url = sqlite:///%(here)s/${TEST_DATABASE_PATH:-test-remarkbox.sqlite}
#sqlalchemy.url = postgres://localhost/remarkbox_test
###
From 2f8e676f780504a0909f19c1d138a35fcc3ca0c0 Mon Sep 17 00:00:00 2001
From: "russell@unturf.com"
Date: Thu, 22 Jan 2026 12:48:02 -0500
Subject: [PATCH 056/181] Add template syntax validation tests
Validates all Jinja2 templates can be parsed without syntax errors.
Catches issues like mismatched {% if %}/{% endif %} blocks before deployment.
---
remarkbox/tests/test_templates.py | 91 +++++++++++++++++++++++++++++++
1 file changed, 91 insertions(+)
create mode 100644 remarkbox/tests/test_templates.py
diff --git a/remarkbox/tests/test_templates.py b/remarkbox/tests/test_templates.py
new file mode 100644
index 0000000..93cfc08
--- /dev/null
+++ b/remarkbox/tests/test_templates.py
@@ -0,0 +1,91 @@
+"""
+Template validation tests.
+
+These tests ensure all Jinja2 templates have valid syntax and can be compiled.
+This catches errors like mismatched {% if %}/{% endif %} blocks before deployment.
+"""
+import os
+import unittest
+from pathlib import Path
+
+from jinja2 import Environment, FileSystemLoader, TemplateSyntaxError
+
+
+class TestTemplateValidation(unittest.TestCase):
+ """Test that all Jinja2 templates have valid syntax."""
+
+ @classmethod
+ def setUpClass(cls):
+ """Set up Jinja2 environment for template testing."""
+ # Find templates directory
+ tests_dir = Path(__file__).parent
+ templates_dir = tests_dir.parent / "templates"
+ cls.templates_dir = templates_dir
+
+ # Create Jinja2 environment matching the app configuration
+ cls.env = Environment(
+ loader=FileSystemLoader(str(templates_dir)),
+ # Don't fail on undefined variables during syntax check
+ autoescape=True,
+ )
+
+ def test_all_templates_have_valid_syntax(self):
+ """
+ Verify all .j2 templates can be parsed without syntax errors.
+
+ This catches issues like:
+ - Mismatched {% if %}/{% endif %} blocks
+ - Unclosed {% block %} tags
+ - Invalid Jinja2 syntax
+ """
+ errors = []
+
+ # Walk through all templates
+ for root, dirs, files in os.walk(self.templates_dir):
+ for filename in files:
+ if filename.endswith(".j2"):
+ # Get relative path for Jinja2 loader
+ full_path = Path(root) / filename
+ rel_path = full_path.relative_to(self.templates_dir)
+ template_name = str(rel_path)
+
+ try:
+ # Attempt to parse the template
+ self.env.get_template(template_name)
+ except TemplateSyntaxError as e:
+ errors.append(
+ f"{template_name}:{e.lineno}: {e.message}"
+ )
+
+ if errors:
+ self.fail(
+ f"Template syntax errors found:\n" + "\n".join(errors)
+ )
+
+ def test_base_template_exists(self):
+ """Verify the base template exists and is valid."""
+ try:
+ template = self.env.get_template("base.j2")
+ self.assertIsNotNone(template)
+ except TemplateSyntaxError as e:
+ self.fail(f"base.j2 has syntax error at line {e.lineno}: {e.message}")
+
+ def test_home_template_exists(self):
+ """Verify home.j2 template exists and is valid."""
+ try:
+ template = self.env.get_template("home.j2")
+ self.assertIsNotNone(template)
+ except TemplateSyntaxError as e:
+ self.fail(f"home.j2 has syntax error at line {e.lineno}: {e.message}")
+
+ def test_show_node_template_exists(self):
+ """Verify show-node.j2 template exists and is valid."""
+ try:
+ template = self.env.get_template("show-node.j2")
+ self.assertIsNotNone(template)
+ except TemplateSyntaxError as e:
+ self.fail(f"show-node.j2 has syntax error at line {e.lineno}: {e.message}")
+
+
+if __name__ == "__main__":
+ unittest.main()
From 67648f06f948f70da24c71f1535f8e5e457de5cb Mon Sep 17 00:00:00 2001
From: "russell@unturf.com"
Date: Sat, 24 Jan 2026 08:04:37 -0500
Subject: [PATCH 057/181] Persist preview visibility state in localStorage
---
remarkbox/static/js/custom.js | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/remarkbox/static/js/custom.js b/remarkbox/static/js/custom.js
index cf83576..92c5df1 100644
--- a/remarkbox/static/js/custom.js
+++ b/remarkbox/static/js/custom.js
@@ -90,7 +90,7 @@ function toggle(target, button, off_text, on_text) {
}
}
-// Animate close for preview-details elements.
+// Animate close for preview-details elements and persist state in localStorage.
if (typeof document.addEventListener === 'function') {
document.addEventListener('click', function(e) {
var summary = e.target.closest('.preview-toggle');
@@ -100,10 +100,13 @@ if (typeof document.addEventListener === 'function') {
if (details.open && !details.classList.contains('closing')) {
e.preventDefault();
details.classList.add('closing');
+ localStorage.setItem('remarkbox-preview-hidden', 'true');
setTimeout(function() {
details.open = false;
details.classList.remove('closing');
}, 800);
+ } else if (!details.open) {
+ localStorage.removeItem('remarkbox-preview-hidden');
}
}, true);
}
@@ -118,6 +121,13 @@ function autoGrow(el) {
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', function() {
+ // Restore preview state from localStorage
+ if (localStorage.getItem('remarkbox-preview-hidden') === 'true') {
+ document.querySelectorAll('.preview-details').forEach(function(details) {
+ details.open = false;
+ });
+ }
+
// Auto-grow textareas on input
document.querySelectorAll('.common-textarea').forEach(function(textarea) {
textarea.addEventListener('input', function() {
From 8971a46659aa34a12ff3c23715a402e4e188f8c0 Mon Sep 17 00:00:00 2001
From: "russell@unturf.com"
Date: Sat, 24 Jan 2026 08:35:35 -0500
Subject: [PATCH 058/181] Hide preview toggle when JavaScript is disabled
---
remarkbox/templates/snippets/forms.j2 | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/remarkbox/templates/snippets/forms.j2 b/remarkbox/templates/snippets/forms.j2
index 27b717d..4be6014 100644
--- a/remarkbox/templates/snippets/forms.j2
+++ b/remarkbox/templates/snippets/forms.j2
@@ -26,7 +26,7 @@
{% set submit_button_value = 'save message' %}
{% include 'submit.j2' %}
-
+ hide preview ▲show preview ▼
From 32b1eae8400afd2523303574a6e66b671098b1d9 Mon Sep 17 00:00:00 2001
From: "russell@unturf.com"
Date: Wed, 28 Jan 2026 07:50:45 -0500
Subject: [PATCH 059/181] Fix getElementById('') warning when no hash fragment
present
---
.../js/iframe-resizer/iframeResizer.contentWindow.js | 2 +-
.../iframe-resizer/iframeResizer.contentWindow.min.js | 11 +----------
.../iframeResizer.contentWindow.min.js.map | 1 +
remarkbox/static/js/iframe-resizer/iframeResizer.js | 2 +-
.../static/js/iframe-resizer/iframeResizer.min.js | 10 +---------
.../static/js/iframe-resizer/iframeResizer.min.js.map | 1 +
6 files changed, 6 insertions(+), 21 deletions(-)
create mode 100644 remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.min.js.map
create mode 100644 remarkbox/static/js/iframe-resizer/iframeResizer.min.js.map
diff --git a/remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.js b/remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.js
index 4b6c789..9d200ae 100644
--- a/remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.js
+++ b/remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.js
@@ -415,7 +415,7 @@
var
hash = location.split('#')[1] || location, //Remove # if present
hashData = decodeURIComponent(hash),
- target = document.getElementById(hashData) || document.getElementsByName(hashData)[0];
+ target = hashData ? (document.getElementById(hashData) || document.getElementsByName(hashData)[0]) : null;
if (undefined !== target){
jumpToTarget(target);
diff --git a/remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.min.js b/remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.min.js
index 2b6760e..7071339 100644
--- a/remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.min.js
+++ b/remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.min.js
@@ -1,10 +1 @@
-/*! iFrame Resizer (iframeSizer.contentWindow.min.js) - v3.5.14 - 2017-03-30
- * Desc: Include this file in any page being loaded into an iframe
- * to force the iframe to resize to the content size.
- * Requires: iframeResizer.min.js on host page.
- * Copyright: (c) 2017 David J. Bradshaw - dave@bradshaw.net
- * License: MIT
- */
-
-!function(a){"use strict";function b(a,b,c){"addEventListener"in window?a.addEventListener(b,c,!1):"attachEvent"in window&&a.attachEvent("on"+b,c)}function c(a,b,c){"removeEventListener"in window?a.removeEventListener(b,c,!1):"detachEvent"in window&&a.detachEvent("on"+b,c)}function d(a){return a.charAt(0).toUpperCase()+a.slice(1)}function e(a){var b,c,d,e=null,f=0,g=function(){f=Ha(),e=null,d=a.apply(b,c),e||(b=c=null)};return function(){var h=Ha();f||(f=h);var i=xa-(h-f);return b=this,c=arguments,0>=i||i>xa?(e&&(clearTimeout(e),e=null),f=h,d=a.apply(b,c),e||(b=c=null)):e||(e=setTimeout(g,i)),d}}function f(a){return ma+"["+oa+"] "+a}function g(a){la&&"object"==typeof window.console&&console.log(f(a))}function h(a){"object"==typeof window.console&&console.warn(f(a))}function i(){j(),g("Initialising iFrame ("+location.href+")"),k(),n(),m("background",W),m("padding",$),A(),s(),t(),o(),C(),u(),ia=B(),N("init","Init message from host page"),Da()}function j(){function b(a){return"true"===a?!0:!1}var c=ha.substr(na).split(":");oa=c[0],X=a!==c[1]?Number(c[1]):X,_=a!==c[2]?b(c[2]):_,la=a!==c[3]?b(c[3]):la,ja=a!==c[4]?Number(c[4]):ja,U=a!==c[6]?b(c[6]):U,Y=c[7],fa=a!==c[8]?c[8]:fa,W=c[9],$=c[10],ua=a!==c[11]?Number(c[11]):ua,ia.enable=a!==c[12]?b(c[12]):!1,qa=a!==c[13]?c[13]:qa,Aa=a!==c[14]?c[14]:Aa}function k(){function a(){var a=window.iFrameResizer;g("Reading data from page: "+JSON.stringify(a)),Ca="messageCallback"in a?a.messageCallback:Ca,Da="readyCallback"in a?a.readyCallback:Da,ta="targetOrigin"in a?a.targetOrigin:ta,fa="heightCalculationMethod"in a?a.heightCalculationMethod:fa,Aa="widthCalculationMethod"in a?a.widthCalculationMethod:Aa}function b(a,b){return"function"==typeof a&&(g("Setup custom "+b+"CalcMethod"),Fa[b]=a,a="custom"),a}"iFrameResizer"in window&&Object===window.iFrameResizer.constructor&&(a(),fa=b(fa,"height"),Aa=b(Aa,"width")),g("TargetOrigin for parent set to: "+ta)}function l(a,b){return-1!==b.indexOf("-")&&(h("Negative CSS value ignored for "+a),b=""),b}function m(b,c){a!==c&&""!==c&&"null"!==c&&(document.body.style[b]=c,g("Body "+b+' set to "'+c+'"'))}function n(){a===Y&&(Y=X+"px"),m("margin",l("margin",Y))}function o(){document.documentElement.style.height="",document.body.style.height="",g('HTML & body height set to "auto"')}function p(a){var e={add:function(c){function d(){N(a.eventName,a.eventType)}Ga[c]=d,b(window,c,d)},remove:function(a){var b=Ga[a];delete Ga[a],c(window,a,b)}};a.eventNames&&Array.prototype.map?(a.eventName=a.eventNames[0],a.eventNames.map(e[a.method])):e[a.method](a.eventName),g(d(a.method)+" event listener: "+a.eventType)}function q(a){p({method:a,eventType:"Animation Start",eventNames:["animationstart","webkitAnimationStart"]}),p({method:a,eventType:"Animation Iteration",eventNames:["animationiteration","webkitAnimationIteration"]}),p({method:a,eventType:"Animation End",eventNames:["animationend","webkitAnimationEnd"]}),p({method:a,eventType:"Input",eventName:"input"}),p({method:a,eventType:"Mouse Up",eventName:"mouseup"}),p({method:a,eventType:"Mouse Down",eventName:"mousedown"}),p({method:a,eventType:"Orientation Change",eventName:"orientationchange"}),p({method:a,eventType:"Print",eventName:["afterprint","beforeprint"]}),p({method:a,eventType:"Ready State Change",eventName:"readystatechange"}),p({method:a,eventType:"Touch Start",eventName:"touchstart"}),p({method:a,eventType:"Touch End",eventName:"touchend"}),p({method:a,eventType:"Touch Cancel",eventName:"touchcancel"}),p({method:a,eventType:"Transition Start",eventNames:["transitionstart","webkitTransitionStart","MSTransitionStart","oTransitionStart","otransitionstart"]}),p({method:a,eventType:"Transition Iteration",eventNames:["transitioniteration","webkitTransitionIteration","MSTransitionIteration","oTransitionIteration","otransitioniteration"]}),p({method:a,eventType:"Transition End",eventNames:["transitionend","webkitTransitionEnd","MSTransitionEnd","oTransitionEnd","otransitionend"]}),"child"===qa&&p({method:a,eventType:"IFrame Resized",eventName:"resize"})}function r(a,b,c,d){return b!==a&&(a in c||(h(a+" is not a valid option for "+d+"CalculationMethod."),a=b),g(d+' calculation method set to "'+a+'"')),a}function s(){fa=r(fa,ea,Ia,"height")}function t(){Aa=r(Aa,za,Ja,"width")}function u(){!0===U?(q("add"),F()):g("Auto Resize disabled")}function v(){g("Disable outgoing messages"),ra=!1}function w(){g("Remove event listener: Message"),c(window,"message",S)}function x(){null!==Z&&Z.disconnect()}function y(){q("remove"),x(),clearInterval(ka)}function z(){v(),w(),!0===U&&y()}function A(){var a=document.createElement("div");a.style.clear="both",a.style.display="block",document.body.appendChild(a)}function B(){function c(){return{x:window.pageXOffset!==a?window.pageXOffset:document.documentElement.scrollLeft,y:window.pageYOffset!==a?window.pageYOffset:document.documentElement.scrollTop}}function d(a){var b=a.getBoundingClientRect(),d=c();return{x:parseInt(b.left,10)+parseInt(d.x,10),y:parseInt(b.top,10)+parseInt(d.y,10)}}function e(b){function c(a){var b=d(a);g("Moving to in page link (#"+e+") at x: "+b.x+" y: "+b.y),R(b.y,b.x,"scrollToOffset")}var e=b.split("#")[1]||b,f=decodeURIComponent(e),h=document.getElementById(f)||document.getElementsByName(f)[0];a!==h?c(h):(g("In page link (#"+e+") not found in iFrame, so sending to parent"),R(0,0,"inPageLink","#"+e))}function f(){""!==location.hash&&"#"!==location.hash&&e(location.href)}function i(){function a(a){function c(a){a.preventDefault(),e(this.getAttribute("href"))}"#"!==a.getAttribute("href")&&b(a,"click",c)}Array.prototype.forEach.call(document.querySelectorAll('a[href^="#"]'),a)}function j(){b(window,"hashchange",f)}function k(){setTimeout(f,ba)}function l(){Array.prototype.forEach&&document.querySelectorAll?(g("Setting up location.hash handlers"),i(),j(),k()):h("In page linking not fully supported in this browser! (See README.md for IE8 workaround)")}return ia.enable?l():g("In page linking not enabled"),{findTarget:e}}function C(){g("Enable public methods"),Ba.parentIFrame={autoResize:function(a){return!0===a&&!1===U?(U=!0,u()):!1===a&&!0===U&&(U=!1,y()),U},close:function(){R(0,0,"close"),z()},getId:function(){return oa},getPageInfo:function(a){"function"==typeof a?(Ea=a,R(0,0,"pageInfo")):(Ea=function(){},R(0,0,"pageInfoStop"))},moveToAnchor:function(a){ia.findTarget(a)},reset:function(){Q("parentIFrame.reset")},scrollTo:function(a,b){R(b,a,"scrollTo")},scrollToOffset:function(a,b){R(b,a,"scrollToOffset")},sendMessage:function(a,b){R(0,0,"message",JSON.stringify(a),b)},setHeightCalculationMethod:function(a){fa=a,s()},setWidthCalculationMethod:function(a){Aa=a,t()},setTargetOrigin:function(a){g("Set targetOrigin: "+a),ta=a},size:function(a,b){var c=""+(a?a:"")+(b?","+b:"");N("size","parentIFrame.size("+c+")",a,b)}}}function D(){0!==ja&&(g("setInterval: "+ja+"ms"),ka=setInterval(function(){N("interval","setInterval: "+ja)},Math.abs(ja)))}function E(){function b(a){function b(a){!1===a.complete&&(g("Attach listeners to "+a.src),a.addEventListener("load",f,!1),a.addEventListener("error",h,!1),k.push(a))}"attributes"===a.type&&"src"===a.attributeName?b(a.target):"childList"===a.type&&Array.prototype.forEach.call(a.target.querySelectorAll("img"),b)}function c(a){k.splice(k.indexOf(a),1)}function d(a){g("Remove listeners from "+a.src),a.removeEventListener("load",f,!1),a.removeEventListener("error",h,!1),c(a)}function e(b,c,e){d(b.target),N(c,e+": "+b.target.src,a,a)}function f(a){e(a,"imageLoad","Image loaded")}function h(a){e(a,"imageLoadFailed","Image load failed")}function i(a){N("mutationObserver","mutationObserver: "+a[0].target+" "+a[0].type),a.forEach(b)}function j(){var a=document.querySelector("body"),b={attributes:!0,attributeOldValue:!1,characterData:!0,characterDataOldValue:!1,childList:!0,subtree:!0};return m=new l(i),g("Create body MutationObserver"),m.observe(a,b),m}var k=[],l=window.MutationObserver||window.WebKitMutationObserver,m=j();return{disconnect:function(){"disconnect"in m&&(g("Disconnect body MutationObserver"),m.disconnect(),k.forEach(d))}}}function F(){var a=0>ja;window.MutationObserver||window.WebKitMutationObserver?a?D():Z=E():(g("MutationObserver not supported in this browser!"),D())}function G(a,b){function c(a){var c=/^\d+(px)?$/i;if(c.test(a))return parseInt(a,V);var d=b.style.left,e=b.runtimeStyle.left;return b.runtimeStyle.left=b.currentStyle.left,b.style.left=a||0,a=b.style.pixelLeft,b.style.left=d,b.runtimeStyle.left=e,a}var d=0;return b=b||document.body,"defaultView"in document&&"getComputedStyle"in document.defaultView?(d=document.defaultView.getComputedStyle(b,null),d=null!==d?d[a]:0):d=c(b.currentStyle[a]),parseInt(d,V)}function H(a){a>xa/2&&(xa=2*a,g("Event throttle increased to "+xa+"ms"))}function I(a,b){for(var c=b.length,e=0,f=0,h=d(a),i=Ha(),j=0;c>j;j++)e=b[j].getBoundingClientRect()[a]+G("margin"+h,b[j]),e>f&&(f=e);return i=Ha()-i,g("Parsed "+c+" HTML elements"),g("Element position calculated in "+i+"ms"),H(i),f}function J(a){return[a.bodyOffset(),a.bodyScroll(),a.documentElementOffset(),a.documentElementScroll()]}function K(a,b){function c(){return h("No tagged elements ("+b+") found on page"),document.querySelectorAll("body *")}var d=document.querySelectorAll("["+b+"]");return 0===d.length&&c(),I(a,d)}function L(){return document.querySelectorAll("body *")}function M(b,c,d,e){function f(){da=m,ya=n,R(da,ya,b)}function h(){function b(a,b){var c=Math.abs(a-b)<=ua;return!c}return m=a!==d?d:Ia[fa](),n=a!==e?e:Ja[Aa](),b(da,m)||_&&b(ya,n)}function i(){return!(b in{init:1,interval:1,size:1})}function j(){return fa in pa||_&&Aa in pa}function k(){g("No change in size detected")}function l(){i()&&j()?Q(c):b in{interval:1}||k()}var m,n;h()||"init"===b?(O(),f()):l()}function N(a,b,c,d){function e(){a in{reset:1,resetPage:1,init:1}||g("Trigger event: "+b)}function f(){return va&&a in aa}f()?g("Trigger event cancelled: "+a):(e(),Ka(a,b,c,d))}function O(){va||(va=!0,g("Trigger event lock on")),clearTimeout(wa),wa=setTimeout(function(){va=!1,g("Trigger event lock off"),g("--")},ba)}function P(a){da=Ia[fa](),ya=Ja[Aa](),R(da,ya,a)}function Q(a){var b=fa;fa=ea,g("Reset trigger event: "+a),O(),P("reset"),fa=b}function R(b,c,d,e,f){function h(){a===f?f=ta:g("Message targetOrigin: "+f)}function i(){var h=b+":"+c,i=oa+":"+h+":"+d+(a!==e?":"+e:"");g("Sending message to host page ("+i+")"),sa.postMessage(ma+i,f)}!0===ra&&(h(),i())}function S(a){function c(){return ma===(""+a.data).substr(0,na)}function d(){return a.data.split("]")[1].split(":")[0]}function e(){return a.data.substr(a.data.indexOf(":")+1)}function f(){return!("undefined"!=typeof module&&module.exports)&&"iFrameResize"in window}function j(){return a.data.split(":")[2]in{"true":1,"false":1}}function k(){var b=d();b in m?m[b]():f()||j()||h("Unexpected message ("+a.data+")")}function l(){!1===ca?k():j()?m.init():g('Ignored message of type "'+d()+'". Received before initialization.')}var m={init:function(){function c(){ha=a.data,sa=a.source,i(),ca=!1,setTimeout(function(){ga=!1},ba)}document.body?c():(g("Waiting for page ready"),b(window,"readystatechange",m.initFromParent))},reset:function(){ga?g("Page reset ignored by init"):(g("Page size reset by host page"),P("resetPage"))},resize:function(){N("resizeParent","Parent window requested size check")},moveToAnchor:function(){ia.findTarget(e())},inPageLink:function(){this.moveToAnchor()},pageInfo:function(){var a=e();g("PageInfoFromParent called from parent: "+a),Ea(JSON.parse(a)),g(" --")},message:function(){var a=e();g("MessageCallback called from parent: "+a),Ca(JSON.parse(a)),g(" --")}};c()&&l()}function T(){"loading"!==document.readyState&&window.parent.postMessage("[iFrameResizerChild]Ready","*")}if("undefined"!=typeof window){var U=!0,V=10,W="",X=0,Y="",Z=null,$="",_=!1,aa={resize:1,click:1},ba=128,ca=!0,da=1,ea="bodyOffset",fa=ea,ga=!0,ha="",ia={},ja=32,ka=null,la=!1,ma="[iFrameSizer]",na=ma.length,oa="",pa={max:1,min:1,bodyScroll:1,documentElementScroll:1},qa="child",ra=!0,sa=window.parent,ta="*",ua=0,va=!1,wa=null,xa=16,ya=1,za="scroll",Aa=za,Ba=window,Ca=function(){h("MessageCallback function not defined")},Da=function(){},Ea=function(){},Fa={height:function(){return h("Custom height calculation function not defined"),document.documentElement.offsetHeight},width:function(){return h("Custom width calculation function not defined"),document.body.scrollWidth}},Ga={},Ha=Date.now||function(){return(new Date).getTime()},Ia={bodyOffset:function(){return document.body.offsetHeight+G("marginTop")+G("marginBottom")},offset:function(){return Ia.bodyOffset()},bodyScroll:function(){return document.body.scrollHeight},custom:function(){return Fa.height()},documentElementOffset:function(){return document.documentElement.offsetHeight},documentElementScroll:function(){return document.documentElement.scrollHeight},max:function(){return Math.max.apply(null,J(Ia))},min:function(){return Math.min.apply(null,J(Ia))},grow:function(){return Ia.max()},lowestElement:function(){return Math.max(Ia.bodyOffset(),I("bottom",L()))},taggedElement:function(){return K("bottom","data-iframe-height")}},Ja={bodyScroll:function(){return document.body.scrollWidth},bodyOffset:function(){return document.body.offsetWidth},custom:function(){return Fa.width()},documentElementScroll:function(){return document.documentElement.scrollWidth},documentElementOffset:function(){return document.documentElement.offsetWidth},scroll:function(){return Math.max(Ja.bodyScroll(),Ja.documentElementScroll())},max:function(){return Math.max.apply(null,J(Ja))},min:function(){return Math.min.apply(null,J(Ja))},rightMostElement:function(){return I("right",L())},taggedElement:function(){return K("right","data-iframe-width")}},Ka=e(M);b(window,"message",S),T()}}();
-//# sourceMappingURL=iframeResizer.contentWindow.map
\ No newline at end of file
+(function(undefined){"use strict";if(typeof window==="undefined")return;var autoResize=true,base=10,bodyBackground="",bodyMargin=0,bodyMarginStr="",bodyObserver=null,bodyPadding="",calculateWidth=false,doubleEventList={resize:1,click:1},eventCancelTimer=128,firstRun=true,height=1,heightCalcModeDefault="bodyOffset",heightCalcMode=heightCalcModeDefault,initLock=true,initMsg="",inPageLinks={},interval=32,intervalTimer=null,logging=false,msgID="[iFrameSizer]",msgIdLen=msgID.length,myID="",observer=null,resetRequiredMethods={max:1,min:1,bodyScroll:1,documentElementScroll:1},resizeFrom="child",sendPermit=true,target=window.parent,targetOriginDefault="*",tolerance=0,triggerLocked=false,triggerLockedTimer=null,throttledTimer=16,width=1,widthCalcModeDefault="scroll",widthCalcMode=widthCalcModeDefault,win=window,messageCallback=function(){warn("MessageCallback function not defined")},readyCallback=function(){},pageInfoCallback=function(){},customCalcMethods={height:function(){warn("Custom height calculation function not defined");return document.documentElement.offsetHeight},width:function(){warn("Custom width calculation function not defined");return document.body.scrollWidth}},eventHandlersByName={};function addEventListener(el,evt,func){if("addEventListener"in window){el.addEventListener(evt,func,false)}else if("attachEvent"in window){el.attachEvent("on"+evt,func)}}function removeEventListener(el,evt,func){if("removeEventListener"in window){el.removeEventListener(evt,func,false)}else if("detachEvent"in window){el.detachEvent("on"+evt,func)}}function capitalizeFirstLetter(string){return string.charAt(0).toUpperCase()+string.slice(1)}function throttle(func){var context,args,result,timeout=null,previous=0,later=function(){previous=getNow();timeout=null;result=func.apply(context,args);if(!timeout){context=args=null}};return function(){var now=getNow();if(!previous){previous=now}var remaining=throttledTimer-(now-previous);context=this;args=arguments;if(remaining<=0||remaining>throttledTimer){if(timeout){clearTimeout(timeout);timeout=null}previous=now;result=func.apply(context,args);if(!timeout){context=args=null}}else if(!timeout){timeout=setTimeout(later,remaining)}return result}}var getNow=Date.now||function(){return(new Date).getTime()};function formatLogMsg(msg){return msgID+"["+myID+"]"+" "+msg}function log(msg){if(logging&&"object"===typeof window.console){console.log(formatLogMsg(msg))}}function warn(msg){if("object"===typeof window.console){console.warn(formatLogMsg(msg))}}function init(){readDataFromParent();log("Initialising iFrame ("+location.href+")");readDataFromPage();setMargin();setBodyStyle("background",bodyBackground);setBodyStyle("padding",bodyPadding);injectClearFixIntoBodyElement();checkHeightMode();checkWidthMode();stopInfiniteResizingOfIFrame();setupPublicMethods();startEventListeners();inPageLinks=setupInPageLinks();sendSize("init","Init message from host page");readyCallback()}function readDataFromParent(){function strBool(str){return"true"===str?true:false}var data=initMsg.substr(msgIdLen).split(":");myID=data[0];bodyMargin=undefined!==data[1]?Number(data[1]):bodyMargin;calculateWidth=undefined!==data[2]?strBool(data[2]):calculateWidth;logging=undefined!==data[3]?strBool(data[3]):logging;interval=undefined!==data[4]?Number(data[4]):interval;autoResize=undefined!==data[6]?strBool(data[6]):autoResize;bodyMarginStr=data[7];heightCalcMode=undefined!==data[8]?data[8]:heightCalcMode;bodyBackground=data[9];bodyPadding=data[10];tolerance=undefined!==data[11]?Number(data[11]):tolerance;inPageLinks.enable=undefined!==data[12]?strBool(data[12]):false;resizeFrom=undefined!==data[13]?data[13]:resizeFrom;widthCalcMode=undefined!==data[14]?data[14]:widthCalcMode}function readDataFromPage(){function readData(){var data=window.iFrameResizer;log("Reading data from page: "+JSON.stringify(data));messageCallback="messageCallback"in data?data.messageCallback:messageCallback;readyCallback="readyCallback"in data?data.readyCallback:readyCallback;targetOriginDefault="targetOrigin"in data?data.targetOrigin:targetOriginDefault;heightCalcMode="heightCalculationMethod"in data?data.heightCalculationMethod:heightCalcMode;widthCalcMode="widthCalculationMethod"in data?data.widthCalculationMethod:widthCalcMode}function setupCustomCalcMethods(calcMode,calcFunc){if("function"===typeof calcMode){log("Setup custom "+calcFunc+"CalcMethod");customCalcMethods[calcFunc]=calcMode;calcMode="custom"}return calcMode}if("iFrameResizer"in window&&Object===window.iFrameResizer.constructor){readData();heightCalcMode=setupCustomCalcMethods(heightCalcMode,"height");widthCalcMode=setupCustomCalcMethods(widthCalcMode,"width")}log("TargetOrigin for parent set to: "+targetOriginDefault)}function chkCSS(attr,value){if(-1!==value.indexOf("-")){warn("Negative CSS value ignored for "+attr);value=""}return value}function setBodyStyle(attr,value){if(undefined!==value&&""!==value&&"null"!==value){document.body.style[attr]=value;log("Body "+attr+' set to "'+value+'"')}}function setMargin(){if(undefined===bodyMarginStr){bodyMarginStr=bodyMargin+"px"}setBodyStyle("margin",chkCSS("margin",bodyMarginStr))}function stopInfiniteResizingOfIFrame(){document.documentElement.style.height="";document.body.style.height="";log('HTML & body height set to "auto"')}function manageTriggerEvent(options){var listener={add:function(eventName){function handleEvent(){sendSize(options.eventName,options.eventType)}eventHandlersByName[eventName]=handleEvent;addEventListener(window,eventName,handleEvent)},remove:function(eventName){var handleEvent=eventHandlersByName[eventName];delete eventHandlersByName[eventName];removeEventListener(window,eventName,handleEvent)}};if(options.eventNames&&Array.prototype.map){options.eventName=options.eventNames[0];options.eventNames.map(listener[options.method])}else{listener[options.method](options.eventName)}log(capitalizeFirstLetter(options.method)+" event listener: "+options.eventType)}function manageEventListeners(method){manageTriggerEvent({method:method,eventType:"Animation Start",eventNames:["animationstart","webkitAnimationStart"]});manageTriggerEvent({method:method,eventType:"Animation Iteration",eventNames:["animationiteration","webkitAnimationIteration"]});manageTriggerEvent({method:method,eventType:"Animation End",eventNames:["animationend","webkitAnimationEnd"]});manageTriggerEvent({method:method,eventType:"Input",eventName:"input"});manageTriggerEvent({method:method,eventType:"Mouse Up",eventName:"mouseup"});manageTriggerEvent({method:method,eventType:"Mouse Down",eventName:"mousedown"});manageTriggerEvent({method:method,eventType:"Orientation Change",eventName:"orientationchange"});manageTriggerEvent({method:method,eventType:"Print",eventName:["afterprint","beforeprint"]});manageTriggerEvent({method:method,eventType:"Ready State Change",eventName:"readystatechange"});manageTriggerEvent({method:method,eventType:"Touch Start",eventName:"touchstart"});manageTriggerEvent({method:method,eventType:"Touch End",eventName:"touchend"});manageTriggerEvent({method:method,eventType:"Touch Cancel",eventName:"touchcancel"});manageTriggerEvent({method:method,eventType:"Transition Start",eventNames:["transitionstart","webkitTransitionStart","MSTransitionStart","oTransitionStart","otransitionstart"]});manageTriggerEvent({method:method,eventType:"Transition Iteration",eventNames:["transitioniteration","webkitTransitionIteration","MSTransitionIteration","oTransitionIteration","otransitioniteration"]});manageTriggerEvent({method:method,eventType:"Transition End",eventNames:["transitionend","webkitTransitionEnd","MSTransitionEnd","oTransitionEnd","otransitionend"]});if("child"===resizeFrom){manageTriggerEvent({method:method,eventType:"IFrame Resized",eventName:"resize"})}}function checkCalcMode(calcMode,calcModeDefault,modes,type){if(calcModeDefault!==calcMode){if(!(calcMode in modes)){warn(calcMode+" is not a valid option for "+type+"CalculationMethod.");calcMode=calcModeDefault}log(type+' calculation method set to "'+calcMode+'"')}return calcMode}function checkHeightMode(){heightCalcMode=checkCalcMode(heightCalcMode,heightCalcModeDefault,getHeight,"height")}function checkWidthMode(){widthCalcMode=checkCalcMode(widthCalcMode,widthCalcModeDefault,getWidth,"width")}function startEventListeners(){if(true===autoResize){manageEventListeners("add");setupMutationObserver()}else{log("Auto Resize disabled")}}function stopMsgsToParent(){log("Disable outgoing messages");sendPermit=false}function removeMsgListener(){log("Remove event listener: Message");removeEventListener(window,"message",receiver)}function disconnectMutationObserver(){if(null!==bodyObserver){bodyObserver.disconnect()}}function stopEventListeners(){manageEventListeners("remove");disconnectMutationObserver();clearInterval(intervalTimer)}function teardown(){stopMsgsToParent();removeMsgListener();if(true===autoResize)stopEventListeners()}function injectClearFixIntoBodyElement(){var clearFix=document.createElement("div");clearFix.style.clear="both";clearFix.style.display="block";document.body.appendChild(clearFix)}function setupInPageLinks(){function getPagePosition(){return{x:window.pageXOffset!==undefined?window.pageXOffset:document.documentElement.scrollLeft,y:window.pageYOffset!==undefined?window.pageYOffset:document.documentElement.scrollTop}}function getElementPosition(el){var elPosition=el.getBoundingClientRect(),pagePosition=getPagePosition();return{x:parseInt(elPosition.left,10)+parseInt(pagePosition.x,10),y:parseInt(elPosition.top,10)+parseInt(pagePosition.y,10)}}function findTarget(location){function jumpToTarget(target){var jumpPosition=getElementPosition(target);log("Moving to in page link (#"+hash+") at x: "+jumpPosition.x+" y: "+jumpPosition.y);sendMsg(jumpPosition.y,jumpPosition.x,"scrollToOffset")}var hash=location.split("#")[1]||location,hashData=decodeURIComponent(hash),target=hashData?document.getElementById(hashData)||document.getElementsByName(hashData)[0]:null;if(undefined!==target){jumpToTarget(target)}else{log("In page link (#"+hash+") not found in iFrame, so sending to parent");sendMsg(0,0,"inPageLink","#"+hash)}}function checkLocationHash(){if(""!==location.hash&&"#"!==location.hash){findTarget(location.href)}}function bindAnchors(){function setupLink(el){function linkClicked(e){e.preventDefault();findTarget(this.getAttribute("href"))}if("#"!==el.getAttribute("href")){addEventListener(el,"click",linkClicked)}}Array.prototype.forEach.call(document.querySelectorAll('a[href^="#"]'),setupLink)}function bindLocationHash(){addEventListener(window,"hashchange",checkLocationHash)}function initCheck(){setTimeout(checkLocationHash,eventCancelTimer)}function enableInPageLinks(){if(Array.prototype.forEach&&document.querySelectorAll){log("Setting up location.hash handlers");bindAnchors();bindLocationHash();initCheck()}else{warn("In page linking not fully supported in this browser! (See README.md for IE8 workaround)")}}if(inPageLinks.enable){enableInPageLinks()}else{log("In page linking not enabled")}return{findTarget:findTarget}}function setupPublicMethods(){log("Enable public methods");win.parentIFrame={autoResize:function autoResizeF(resize){if(true===resize&&false===autoResize){autoResize=true;startEventListeners()}else if(false===resize&&true===autoResize){autoResize=false;stopEventListeners()}return autoResize},close:function closeF(){sendMsg(0,0,"close");teardown()},getId:function getIdF(){return myID},getPageInfo:function getPageInfoF(callback){if("function"===typeof callback){pageInfoCallback=callback;sendMsg(0,0,"pageInfo")}else{pageInfoCallback=function(){};sendMsg(0,0,"pageInfoStop")}},moveToAnchor:function moveToAnchorF(hash){inPageLinks.findTarget(hash)},reset:function resetF(){resetIFrame("parentIFrame.reset")},scrollTo:function scrollToF(x,y){sendMsg(y,x,"scrollTo")},scrollToOffset:function scrollToF(x,y){sendMsg(y,x,"scrollToOffset")},sendMessage:function sendMessageF(msg,targetOrigin){sendMsg(0,0,"message",JSON.stringify(msg),targetOrigin)},setHeightCalculationMethod:function setHeightCalculationMethodF(heightCalculationMethod){heightCalcMode=heightCalculationMethod;checkHeightMode()},setWidthCalculationMethod:function setWidthCalculationMethodF(widthCalculationMethod){widthCalcMode=widthCalculationMethod;checkWidthMode()},setTargetOrigin:function setTargetOriginF(targetOrigin){log("Set targetOrigin: "+targetOrigin);targetOriginDefault=targetOrigin},size:function sizeF(customHeight,customWidth){var valString=""+(customHeight?customHeight:"")+(customWidth?","+customWidth:"");sendSize("size","parentIFrame.size("+valString+")",customHeight,customWidth)}}}function initInterval(){if(0!==interval){log("setInterval: "+interval+"ms");intervalTimer=setInterval((function(){sendSize("interval","setInterval: "+interval)}),Math.abs(interval))}}function setupBodyMutationObserver(){function addImageLoadListners(mutation){function addImageLoadListener(element){if(false===element.complete){log("Attach listeners to "+element.src);element.addEventListener("load",imageLoaded,false);element.addEventListener("error",imageError,false);elements.push(element)}}if(mutation.type==="attributes"&&mutation.attributeName==="src"){addImageLoadListener(mutation.target)}else if(mutation.type==="childList"){Array.prototype.forEach.call(mutation.target.querySelectorAll("img"),addImageLoadListener)}}function removeFromArray(element){elements.splice(elements.indexOf(element),1)}function removeImageLoadListener(element){log("Remove listeners from "+element.src);element.removeEventListener("load",imageLoaded,false);element.removeEventListener("error",imageError,false);removeFromArray(element)}function imageEventTriggered(event,type,typeDesc){removeImageLoadListener(event.target);sendSize(type,typeDesc+": "+event.target.src,undefined,undefined)}function imageLoaded(event){imageEventTriggered(event,"imageLoad","Image loaded")}function imageError(event){imageEventTriggered(event,"imageLoadFailed","Image load failed")}function mutationObserved(mutations){sendSize("mutationObserver","mutationObserver: "+mutations[0].target+" "+mutations[0].type);mutations.forEach(addImageLoadListners)}function createMutationObserver(){var target=document.querySelector("body"),config={attributes:true,attributeOldValue:false,characterData:true,characterDataOldValue:false,childList:true,subtree:true};observer=new MutationObserver(mutationObserved);log("Create body MutationObserver");observer.observe(target,config);return observer}var elements=[],MutationObserver=window.MutationObserver||window.WebKitMutationObserver,observer=createMutationObserver();return{disconnect:function(){if("disconnect"in observer){log("Disconnect body MutationObserver");observer.disconnect();elements.forEach(removeImageLoadListener)}}}}function setupMutationObserver(){var forceIntervalTimer=0>interval;if(window.MutationObserver||window.WebKitMutationObserver){if(forceIntervalTimer){initInterval()}else{bodyObserver=setupBodyMutationObserver()}}else{log("MutationObserver not supported in this browser!");initInterval()}}function getComputedStyle(prop,el){function convertUnitsToPxForIE8(value){var PIXEL=/^\d+(px)?$/i;if(PIXEL.test(value)){return parseInt(value,base)}var style=el.style.left,runtimeStyle=el.runtimeStyle.left;el.runtimeStyle.left=el.currentStyle.left;el.style.left=value||0;value=el.style.pixelLeft;el.style.left=style;el.runtimeStyle.left=runtimeStyle;return value}var retVal=0;el=el||document.body;if("defaultView"in document&&"getComputedStyle"in document.defaultView){retVal=document.defaultView.getComputedStyle(el,null);retVal=null!==retVal?retVal[prop]:0}else{retVal=convertUnitsToPxForIE8(el.currentStyle[prop])}return parseInt(retVal,base)}function chkEventThottle(timer){if(timer>throttledTimer/2){throttledTimer=2*timer;log("Event throttle increased to "+throttledTimer+"ms")}}function getMaxElement(side,elements){var elementsLength=elements.length,elVal=0,maxVal=0,Side=capitalizeFirstLetter(side),timer=getNow();for(var i=0;imaxVal){maxVal=elVal}}timer=getNow()-timer;log("Parsed "+elementsLength+" HTML elements");log("Element position calculated in "+timer+"ms");chkEventThottle(timer);return maxVal}function getAllMeasurements(dimention){return[dimention.bodyOffset(),dimention.bodyScroll(),dimention.documentElementOffset(),dimention.documentElementScroll()]}function getTaggedElements(side,tag){function noTaggedElementsFound(){warn("No tagged elements ("+tag+") found on page");return document.querySelectorAll("body *")}var elements=document.querySelectorAll("["+tag+"]");if(0===elements.length)noTaggedElementsFound();return getMaxElement(side,elements)}function getAllElements(){return document.querySelectorAll("body *")}var getHeight={bodyOffset:function getBodyOffsetHeight(){return document.body.offsetHeight+getComputedStyle("marginTop")+getComputedStyle("marginBottom")},offset:function(){return getHeight.bodyOffset()},bodyScroll:function getBodyScrollHeight(){return document.body.scrollHeight},custom:function getCustomWidth(){return customCalcMethods.height()},documentElementOffset:function getDEOffsetHeight(){return document.documentElement.offsetHeight},documentElementScroll:function getDEScrollHeight(){return document.documentElement.scrollHeight},max:function getMaxHeight(){return Math.max.apply(null,getAllMeasurements(getHeight))},min:function getMinHeight(){return Math.min.apply(null,getAllMeasurements(getHeight))},grow:function growHeight(){return getHeight.max()},lowestElement:function getBestHeight(){return Math.max(getHeight.bodyOffset(),getMaxElement("bottom",getAllElements()))},taggedElement:function getTaggedElementsHeight(){return getTaggedElements("bottom","data-iframe-height")}},getWidth={bodyScroll:function getBodyScrollWidth(){return document.body.scrollWidth},bodyOffset:function getBodyOffsetWidth(){return document.body.offsetWidth},custom:function getCustomWidth(){return customCalcMethods.width()},documentElementScroll:function getDEScrollWidth(){return document.documentElement.scrollWidth},documentElementOffset:function getDEOffsetWidth(){return document.documentElement.offsetWidth},scroll:function getMaxWidth(){return Math.max(getWidth.bodyScroll(),getWidth.documentElementScroll())},max:function getMaxWidth(){return Math.max.apply(null,getAllMeasurements(getWidth))},min:function getMinWidth(){return Math.min.apply(null,getAllMeasurements(getWidth))},rightMostElement:function rightMostElement(){return getMaxElement("right",getAllElements())},taggedElement:function getTaggedElementsWidth(){return getTaggedElements("right","data-iframe-width")}};function sizeIFrame(triggerEvent,triggerEventDesc,customHeight,customWidth){function resizeIFrame(){height=currentHeight;width=currentWidth;sendMsg(height,width,triggerEvent)}function isSizeChangeDetected(){function checkTolarance(a,b){var retVal=Math.abs(a-b)<=tolerance;return!retVal}currentHeight=undefined!==customHeight?customHeight:getHeight[heightCalcMode]();currentWidth=undefined!==customWidth?customWidth:getWidth[widthCalcMode]();return checkTolarance(height,currentHeight)||calculateWidth&&checkTolarance(width,currentWidth)}function isForceResizableEvent(){return!(triggerEvent in{init:1,interval:1,size:1})}function isForceResizableCalcMode(){return heightCalcMode in resetRequiredMethods||calculateWidth&&widthCalcMode in resetRequiredMethods}function logIgnored(){log("No change in size detected")}function checkDownSizing(){if(isForceResizableEvent()&&isForceResizableCalcMode()){resetIFrame(triggerEventDesc)}else if(!(triggerEvent in{interval:1})){logIgnored()}}var currentHeight,currentWidth;if(isSizeChangeDetected()||"init"===triggerEvent){lockTrigger();resizeIFrame()}else{checkDownSizing()}}var sizeIFrameThrottled=throttle(sizeIFrame);function sendSize(triggerEvent,triggerEventDesc,customHeight,customWidth){function recordTrigger(){if(!(triggerEvent in{reset:1,resetPage:1,init:1})){log("Trigger event: "+triggerEventDesc)}}function isDoubleFiredEvent(){return triggerLocked&&triggerEvent in doubleEventList}if(!isDoubleFiredEvent()){recordTrigger();sizeIFrameThrottled(triggerEvent,triggerEventDesc,customHeight,customWidth)}else{log("Trigger event cancelled: "+triggerEvent)}}function lockTrigger(){if(!triggerLocked){triggerLocked=true;log("Trigger event lock on")}clearTimeout(triggerLockedTimer);triggerLockedTimer=setTimeout((function(){triggerLocked=false;log("Trigger event lock off");log("--")}),eventCancelTimer)}function triggerReset(triggerEvent){height=getHeight[heightCalcMode]();width=getWidth[widthCalcMode]();sendMsg(height,width,triggerEvent)}function resetIFrame(triggerEventDesc){var hcm=heightCalcMode;heightCalcMode=heightCalcModeDefault;log("Reset trigger event: "+triggerEventDesc);lockTrigger();triggerReset("reset");heightCalcMode=hcm}function sendMsg(height,width,triggerEvent,msg,targetOrigin){function setTargetOrigin(){if(undefined===targetOrigin){targetOrigin=targetOriginDefault}else{log("Message targetOrigin: "+targetOrigin)}}function sendToParent(){var size=height+":"+width,message=myID+":"+size+":"+triggerEvent+(undefined!==msg?":"+msg:"");log("Sending message to host page ("+message+")");target.postMessage(msgID+message,targetOrigin)}if(true===sendPermit){setTargetOrigin();sendToParent()}}function receiver(event){var processRequestFromParent={init:function initFromParent(){function fireInit(){initMsg=event.data;target=event.source;init();firstRun=false;setTimeout((function(){initLock=false}),eventCancelTimer)}if(document.body){fireInit()}else{log("Waiting for page ready");addEventListener(window,"readystatechange",processRequestFromParent.initFromParent)}},reset:function resetFromParent(){if(!initLock){log("Page size reset by host page");triggerReset("resetPage")}else{log("Page reset ignored by init")}},resize:function resizeFromParent(){sendSize("resizeParent","Parent window requested size check")},moveToAnchor:function moveToAnchorF(){inPageLinks.findTarget(getData())},inPageLink:function inPageLinkF(){this.moveToAnchor()},pageInfo:function pageInfoFromParent(){var msgBody=getData();log("PageInfoFromParent called from parent: "+msgBody);pageInfoCallback(JSON.parse(msgBody));log(" --")},message:function messageFromParent(){var msgBody=getData();log("MessageCallback called from parent: "+msgBody);messageCallback(JSON.parse(msgBody));log(" --")}};function isMessageForUs(){return msgID===(""+event.data).substr(0,msgIdLen)}function getMessageType(){return event.data.split("]")[1].split(":")[0]}function getData(){return event.data.substr(event.data.indexOf(":")+1)}function isMiddleTier(){return!(typeof module!=="undefined"&&module.exports)&&"iFrameResize"in window}function isInitMsg(){return event.data.split(":")[2]in{true:1,false:1}}function callFromParent(){var messageType=getMessageType();if(messageType in processRequestFromParent){processRequestFromParent[messageType]()}else if(!isMiddleTier()&&!isInitMsg()){warn("Unexpected message ("+event.data+")")}}function processMessage(){if(false===firstRun){callFromParent()}else if(isInitMsg()){processRequestFromParent.init()}else{log('Ignored message of type "'+getMessageType()+'". Received before initialization.')}}if(isMessageForUs()){processMessage()}}function chkLateLoaded(){if("loading"!==document.readyState){window.parent.postMessage("[iFrameResizerChild]Ready","*")}}addEventListener(window,"message",receiver);chkLateLoaded()})();
\ No newline at end of file
diff --git a/remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.min.js.map b/remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.min.js.map
new file mode 100644
index 0000000..38c9a90
--- /dev/null
+++ b/remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"iframeResizer.contentWindow.min.js.map","names":["undefined","window","autoResize","base","bodyBackground","bodyMargin","bodyMarginStr","bodyObserver","bodyPadding","calculateWidth","doubleEventList","resize","click","eventCancelTimer","firstRun","height","heightCalcModeDefault","heightCalcMode","initLock","initMsg","inPageLinks","interval","intervalTimer","logging","msgID","msgIdLen","length","myID","observer","resetRequiredMethods","max","min","bodyScroll","documentElementScroll","resizeFrom","sendPermit","target","parent","targetOriginDefault","tolerance","triggerLocked","triggerLockedTimer","throttledTimer","width","widthCalcModeDefault","widthCalcMode","win","messageCallback","warn","readyCallback","pageInfoCallback","customCalcMethods","document","documentElement","offsetHeight","body","scrollWidth","eventHandlersByName","addEventListener","el","evt","func","attachEvent","removeEventListener","detachEvent","capitalizeFirstLetter","string","charAt","toUpperCase","slice","throttle","context","args","result","timeout","previous","later","getNow","apply","now","remaining","this","arguments","clearTimeout","setTimeout","Date","getTime","formatLogMsg","msg","log","console","init","readDataFromParent","location","href","readDataFromPage","setMargin","setBodyStyle","injectClearFixIntoBodyElement","checkHeightMode","checkWidthMode","stopInfiniteResizingOfIFrame","setupPublicMethods","startEventListeners","setupInPageLinks","sendSize","strBool","str","data","substr","split","Number","enable","readData","iFrameResizer","JSON","stringify","targetOrigin","heightCalculationMethod","widthCalculationMethod","setupCustomCalcMethods","calcMode","calcFunc","Object","constructor","chkCSS","attr","value","indexOf","style","manageTriggerEvent","options","listener","add","eventName","handleEvent","eventType","remove","eventNames","Array","prototype","map","method","manageEventListeners","checkCalcMode","calcModeDefault","modes","type","getHeight","getWidth","setupMutationObserver","stopMsgsToParent","removeMsgListener","receiver","disconnectMutationObserver","disconnect","stopEventListeners","clearInterval","teardown","clearFix","createElement","clear","display","appendChild","getPagePosition","x","pageXOffset","scrollLeft","y","pageYOffset","scrollTop","getElementPosition","elPosition","getBoundingClientRect","pagePosition","parseInt","left","top","findTarget","jumpToTarget","jumpPosition","hash","sendMsg","hashData","decodeURIComponent","getElementById","getElementsByName","checkLocationHash","bindAnchors","setupLink","linkClicked","e","preventDefault","getAttribute","forEach","call","querySelectorAll","bindLocationHash","initCheck","enableInPageLinks","parentIFrame","autoResizeF","close","closeF","getId","getIdF","getPageInfo","getPageInfoF","callback","moveToAnchor","moveToAnchorF","reset","resetF","resetIFrame","scrollTo","scrollToF","scrollToOffset","sendMessage","sendMessageF","setHeightCalculationMethod","setHeightCalculationMethodF","setWidthCalculationMethod","setWidthCalculationMethodF","setTargetOrigin","setTargetOriginF","size","sizeF","customHeight","customWidth","valString","initInterval","setInterval","Math","abs","setupBodyMutationObserver","addImageLoadListners","mutation","addImageLoadListener","element","complete","src","imageLoaded","imageError","elements","push","attributeName","removeFromArray","splice","removeImageLoadListener","imageEventTriggered","event","typeDesc","mutationObserved","mutations","createMutationObserver","querySelector","config","attributes","attributeOldValue","characterData","characterDataOldValue","childList","subtree","MutationObserver","observe","WebKitMutationObserver","forceIntervalTimer","getComputedStyle","prop","convertUnitsToPxForIE8","PIXEL","test","runtimeStyle","currentStyle","pixelLeft","retVal","defaultView","chkEventThottle","timer","getMaxElement","side","elementsLength","elVal","maxVal","Side","i","getAllMeasurements","dimention","bodyOffset","documentElementOffset","getTaggedElements","tag","noTaggedElementsFound","getAllElements","getBodyOffsetHeight","offset","getBodyScrollHeight","scrollHeight","custom","getCustomWidth","getDEOffsetHeight","getDEScrollHeight","getMaxHeight","getMinHeight","grow","growHeight","lowestElement","getBestHeight","taggedElement","getTaggedElementsHeight","getBodyScrollWidth","getBodyOffsetWidth","offsetWidth","getDEScrollWidth","getDEOffsetWidth","scroll","getMaxWidth","getMinWidth","rightMostElement","getTaggedElementsWidth","sizeIFrame","triggerEvent","triggerEventDesc","resizeIFrame","currentHeight","currentWidth","isSizeChangeDetected","checkTolarance","a","b","isForceResizableEvent","isForceResizableCalcMode","logIgnored","checkDownSizing","lockTrigger","sizeIFrameThrottled","recordTrigger","resetPage","isDoubleFiredEvent","triggerReset","hcm","sendToParent","message","postMessage","processRequestFromParent","initFromParent","fireInit","source","resetFromParent","resizeFromParent","getData","inPageLink","inPageLinkF","pageInfo","pageInfoFromParent","msgBody","parse","messageFromParent","isMessageForUs","getMessageType","isMiddleTier","module","exports","isInitMsg","true","false","callFromParent","messageType","processMessage","chkLateLoaded","readyState"],"sources":["iframeResizer.contentWindow.js"],"mappings":"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"}
\ No newline at end of file
diff --git a/remarkbox/static/js/iframe-resizer/iframeResizer.js b/remarkbox/static/js/iframe-resizer/iframeResizer.js
index a8a6995..bc23242 100644
--- a/remarkbox/static/js/iframe-resizer/iframeResizer.js
+++ b/remarkbox/static/js/iframe-resizer/iframeResizer.js
@@ -413,7 +413,7 @@
var
hash = location.split('#')[1] || '',
hashData = decodeURIComponent(hash),
- target = document.getElementById(hashData) || document.getElementsByName(hashData)[0];
+ target = hashData ? (document.getElementById(hashData) || document.getElementsByName(hashData)[0]) : null;
if (target){
jumpToTarget();
diff --git a/remarkbox/static/js/iframe-resizer/iframeResizer.min.js b/remarkbox/static/js/iframe-resizer/iframeResizer.min.js
index 7fe4b74..68023b7 100644
--- a/remarkbox/static/js/iframe-resizer/iframeResizer.min.js
+++ b/remarkbox/static/js/iframe-resizer/iframeResizer.min.js
@@ -1,9 +1 @@
-/*! iFrame Resizer (iframeSizer.min.js ) - v3.5.14 - 2017-03-30
- * Desc: Force cross domain iframes to size to content.
- * Requires: iframeResizer.contentWindow.min.js to be loaded into the target frame.
- * Copyright: (c) 2017 David J. Bradshaw - dave@bradshaw.net
- * License: MIT
- */
-
-!function(a){"use strict";function b(a,b,c){"addEventListener"in window?a.addEventListener(b,c,!1):"attachEvent"in window&&a.attachEvent("on"+b,c)}function c(a,b,c){"removeEventListener"in window?a.removeEventListener(b,c,!1):"detachEvent"in window&&a.detachEvent("on"+b,c)}function d(){var a,b=["moz","webkit","o","ms"];for(a=0;ae&&(e=c,h(V,"Set "+d+" to min value")),e>b&&(e=b,h(V,"Set "+d+" to max value")),U[d]=""+e}function g(){function b(){function a(){var a=0,b=!1;for(h(V,"Checking connection is from allowed list of origins: "+d);aP[x]["max"+a])throw new Error("Value for min"+a+" can not be greater than max"+a)}b("Height"),b("Width"),a("maxHeight"),a("minHeight"),a("maxWidth"),a("minWidth")}function f(){var a=d&&d.id||S.id+F++;return null!==document.getElementById(a)&&(a+=F++),a}function g(a){return R=a,""===a&&(c.id=a=f(),G=(d||{}).log,R=a,h(a,"Added missing iframe ID: "+a+" ("+c.src+")")),a}function i(){switch(h(x,"IFrame scrolling "+(P[x].scrolling?"enabled":"disabled")+" for "+x),c.style.overflow=!1===P[x].scrolling?"hidden":"auto",P[x].scrolling){case!0:c.scrolling="yes";break;case!1:c.scrolling="no";break;default:c.scrolling=P[x].scrolling}}function k(){("number"==typeof P[x].bodyMargin||"0"===P[x].bodyMargin)&&(P[x].bodyMarginV1=P[x].bodyMargin,P[x].bodyMargin=""+P[x].bodyMargin+"px")}function l(){var a=P[x].firstRun,b=P[x].heightCalculationMethod in O;!a&&b&&r({iframe:c,height:0,width:0,type:"init"})}function m(){Function.prototype.bind&&(P[x].iframe.iFrameResizer={close:n.bind(null,P[x].iframe),resize:u.bind(null,"Window resize","resize",P[x].iframe),moveToAnchor:function(a){u("Move to anchor","moveToAnchor:"+a,P[x].iframe,x)},sendMessage:function(a){a=JSON.stringify(a),u("Send Message","message:"+a,P[x].iframe,x)}})}function o(d){function e(){u("iFrame.onload",d,c,a,!0),l()}b(c,"load",e),u("init",d,c,a,!0)}function p(a){if("object"!=typeof a)throw new TypeError("Options is not an object")}function q(a){for(var b in S)S.hasOwnProperty(b)&&(P[x][b]=a.hasOwnProperty(b)?a[b]:S[b])}function s(a){return""===a||"file://"===a?"*":a}function t(a){a=a||{},P[x]={firstRun:!0,iframe:c,remoteHost:c.src.split("/").slice(0,3).join("/")},p(a),q(a),P[x].targetOrigin=!0===P[x].checkOrigin?s(P[x].remoteHost):"*"}function w(){return x in P&&"iFrameResizer"in c}var x=g(c.id);w()?j(x,"Ignored iFrame, already setup."):(t(d),i(),e(),k(),o(v(x)),m())}function x(a,b){null===Q&&(Q=setTimeout(function(){Q=null,a()},b))}function y(){function a(){function a(a){function b(b){return"0px"===P[a].iframe.style[b]}function c(a){return null!==a.offsetParent}c(P[a].iframe)&&(b("height")||b("width"))&&u("Visibility change","resize",P[a].iframe,a)}for(var b in P)a(b)}function b(b){h("window","Mutation observed: "+b[0].target+" "+b[0].type),x(a,16)}function c(){var a=document.querySelector("body"),c={attributes:!0,attributeOldValue:!1,characterData:!0,characterDataOldValue:!1,childList:!0,subtree:!0},e=new d(b);e.observe(a,c)}var d=window.MutationObserver||window.WebKitMutationObserver;d&&c()}function z(a){function b(){B("Window "+a,"resize")}h("window","Trigger event: "+a),x(b,16)}function A(){function a(){B("Tab Visable","resize")}"hidden"!==document.visibilityState&&(h("document","Trigger event: Visiblity change"),x(a,16))}function B(a,b){function c(a){return"parent"===P[a].resizeFrom&&P[a].autoResize&&!P[a].firstRun}for(var d in P)c(d)&&u(a,b,document.getElementById(d),d)}function C(){b(window,"message",l),b(window,"resize",function(){z("resize")}),b(document,"visibilitychange",A),b(document,"-webkit-visibilitychange",A),b(window,"focusin",function(){z("focus")}),b(window,"focus",function(){z("focus")})}function D(){function b(a,b){function c(){if(!b.tagName)throw new TypeError("Object is not a valid DOM element");if("IFRAME"!==b.tagName.toUpperCase())throw new TypeError("Expected