A decision is progress, right?
Kevin uttered this mantra and now it’s in my repertoire. There are multiple times in
Kevin uttered this mantra and now it’s in my repertoire. There are multiple times in
This week I had a really horrible bug. One spec in our suite in our CI knocked out every subsequent spec after skipping. The spec works in isolation. just not on CI and not in a full suite run.
RSpec.shared_contexts 'with some feature' do
skip('This is not available on CI') if ENV['CI']
original_driver = Capybara.javascript_driver
Capybara.javascript_driver = :something_else
ensure
Capybara.javascript_driver = original_driver
end
RSpec.describe 'something' do
include_context 'with some feature'
it do
visit '/'
end
end
Turns out our Capybara.javascript_driver
ended up being nil, and our test suite fell over because CI doesn’t support non-headless Chrome. And the reason…
I would appear that variable assignment in Ruby still assigns nil
if we attempt to assign a variable from another variable or method that is undefined.
irb(main):001> a = some_method_or_var_that_is_not_defined
(irb):1:in `<main>': undefined local variable or method `some_method_or_var_that_is_not_defined' for main (NameError)
a = some_method_or_var_that_is_not_defined
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
from <internal:kernel>:187:in `loop'
from /Users/rl/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/irb-1.14.0/exe/irb:9:in `<top (required)>'
from /Users/rl/.asdf/installs/ruby/3.3.0/bin/irb:25:in `load'
from /Users/rl/.asdf/installs/ruby/3.3.0/bin/irb:25:in `<main>'
irb(main):002> a
=> nil
Who knew? I didn’t.
Just deployed a new Ruby 3.3.0 branch to staging and it would appear that our Resque jobs just stopped working. Usual procedure check the logs, look for an error and backtrace. Not logs. Ok there is no log/resque.log
. There is nothing in the staging.log
. Ok. So the process that kicks off our service is monit
. Monit log.
/var/log/monit.log
[UTC Feb 21 09:12:46] error : 'resque-scheduler' process is not running
[UTC Feb 21 09:12:46] info : 'resque-scheduler' trying to restart
[UTC Feb 21 09:12:46] info : 'resque-scheduler' start: '/etc/init.d/resque-scheduler start'
[UTC Feb 21 09:13:16] error : 'resque-scheduler' failed to start (exit status 0) -- no output
[UTC Feb 21 09:15:16] error : 'resque' process is not running
[UTC Feb 21 09:15:16] info : 'resque' trying to restart
[UTC Feb 21 09:15:16] info : 'resque' start: '/etc/init.d/resque start'
[UTC Feb 21 09:15:46] error : 'resque' failed to start (exit status 0) -- '/etc/init.d/resque start': /etc/init.d/resque: 12: kill: No such process
Brilliant, it states it has exit status 0 and nothing else. OK. Script tells us that it should boot the process, nothing out of the ordinary here. Nothing to indicate that it is locked to an older Ruby version or anything.
/etc/init.d/resque
PID=/home/deploy/app/shared/pids/resque.pid
CMD="cd /home/deploy/app/current; PIDFILE=$PID BACKGROUND=yes bundle exec rake environment resque:work QUEUE=* RAILS_ENV=production"
echo $CMD
It is backgrounded though, if there was an error it wouldn’t end up in STDOUT. Ok, how about we boot without the background option.
$ bundle exec rake environment resque:work QUEUE=* RAILS_ENV=staging
rake aborted!
NoMethodError: undefined method `[]' for nil
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/resque-2.0.0/lib/resque/logging.rb:8:in `log'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/resque-2.0.0/lib/resque/logging.rb:13:in `info'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/resque-2.0.0/lib/resque/worker.rb:854:in `log'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/resque-2.0.0/lib/resque/tasks.rb:19:in `block (2 levels) in <main>'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/rake-13.0.6/exe/rake:27:in `<top (required)>'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/bundler-2.5.5/lib/bundler/cli/exec.rb:58:in `load'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/bundler-2.5.5/lib/bundler/cli/exec.rb:58:in `kernel_load'
Ah, there is is. So the Resque::Logging.log proxies onto the Resque.logger if it is defined.
module Resque
# Include this module in classes you wish to have logging facilities
module Logging
module_function
# Thunk to the logger's own log method (if configured)
def self.log(severity, message)
Resque.logger.__send__(severity, message) if Resque.logger
end
# Log level aliases
def debug(message); Logging.log :debug, message; end
def info(message); Logging.log :info, message; end
def warn(message); Logging.log :warn, message; end
def error(message); Logging.log :error, message; end
def fatal(message); Logging.log :fatal, message; end
end
end
Let’s try it.
% rails c
Loading development environment (Rails 6.1.7)
rirb: warn: can't alias measure from irb_measure.
irb(main):001> Resque.logger.debug('blah')
/Users/rl/.asdf/installs/ruby/3.3.0/lib/ruby/3.3.0/logger.rb:384:in `level': undefined method `[]' for nil (NoMethodError)
@level_override[Fiber.current] || @level
Ah, that’s a problem. The Resque.logger we’re using seem incompatable with Logger.
And we’re not the only ones to see this. https://talk.jekyllrb.com/t/error-when-executing-bundle-install/8822/6
It would appear that Logger has changed, there is a fix in Jekyll 4.3.3 for their issue. So what’s our issue.
https://github.com/jekyll/jekyll/commit/595cc230678952fab244a62dc1811ea39ec34041
Resque by default defines the Resque.logger
as an instance of MonoLogger
. Never heard of it.
lib/resque.rb
# Log to STDOUT by default
Resque.logger = MonoLogger.new(STDOUT)
Resque.logger.formatter = Resque::QuietFormatter.new
Our Gemfile.lock reckons we’re using 1.1.1
of mono_logger
Gemfile.lock
mongoid_paranoia (0.5.0)
mongoid (~> 7.3)
mono_logger (1.1.1)
msgpack (1.7.2)
multi_json (1.15.0)
multi_xml (0.6.0)
There is a new patch version of mono_logger
available. And there it is in the commit history.
https://github.com/steveklabnik/mono_logger/commit/14bfb8302609123d702ace527c9114a8ff7c438a
They have also fixed it. Logger initializer has changed, so if you’re going to use it sub-class Logger use their initializer. Upgrading to bundle update mono_logger
bumps the version to 1.1.2
and fixed our issue.
Honestly, Ruby 3.2.2 to Ruby 3.3.0 upgrades have been really painless. I’ve hit every server and installed Ruby 3.3.0 with YJIT enabled and we’re ready to go as soon as the new Ruby_3.3.0 branches specs pass.
However, this one is rather annoying.
% bundle install
Fetching gem metadata from https://rubygems.org/........
Resolving dependencies....................
--- ERROR REPORT TEMPLATE -------------------------------------------------------
NoMethodError: undefined method `version' for an instance of Bundler::IncompleteSpecification
This current Gemfile.lock indicates we last bundled this with Bunder 2.3.20. I know the latest version is 2.5.5
RUBY VERSION
ruby 3.2.2p53
BUNDLED WITH
2.3.20
Ok in the Gemfile we have locked to a specific version of bundler, I’m not sure why. I’d think that bundler is pretty safe to allow the host system to have whatever version is installed manage this.
gem 'bundler', '~> 2.3.8'
If I remove this line from the Gemfile, nothing changes. I still get Unfortunately, an unexpected error occurred, and Bundler cannot continue.
It’s expecting that version of bundler to manage it still.
Bundler 2.3.20
Platforms ruby, arm64-darwin-23
Ruby 3.3.0p0 (2023-12-25 revision 5124f9ac7513eb590c37717337c430cb93caa151) [arm64-darwin-23]
Full Path /Users/rl/.asdf/installs/ruby/3.3.0/bin/ruby
Config Dir /Users/rl/.asdf/installs/ruby/3.3.0/etc
RubyGems 3.5.3
Gem Home /Users/rl/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0
Gem Path /Users/rl/.gem/ruby/3.3.0:/Users/rl/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0
User Home /Users/rl
User Path /Users/rl/.gem/ruby/3.3.0
Bin Dir /Users/rl/.asdf/installs/ruby/3.3.0/bin
OpenSSL
Compiled OpenSSL 3.2.0 23 Nov 2023
Loaded OpenSSL 3.2.0 23 Nov 2023
Cert File /opt/homebrew/etc/openssl@3/cert.pem
Cert Dir /opt/homebrew/etc/openssl@3/certs
Tools
Git 2.39.3 (Apple Git-145)
RVM not installed
rbenv not installed
chruby not installed
Even if 2.5.5 is the default.
% gem list bundler
*** LOCAL GEMS ***
bundler (2.5.5, default: 2.5.3, 2.4.21, 2.4.0, 2.3.20)
bundler-audit (0.9.1)
capistrano-bundler (2.1.0, 2.0.1)
So I have to force bundler to use the correct version for this bundle install
to work. And success.
bundle _2.5.5_ install
---8<---
Bundle complete! 115 Gemfile dependencies, 321 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
How hard is it to keep my Rails blog up to date? Honestly, I shouldn’t be worrying about such things because I created this site so many years ago and it was running Rails 4.x up until about a year ago. I them upgraded it to 7.x and everything broke. So now I’m taking a little time to spring clean and today…
rl@loathsome robl.me % bundle outdated
Fetching https://github.com/braindeaf/capistrano-puma
Fetching git@github.com:braindeaf/m3ta.git
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Gem Current Latest Requested Groups
http-accept 1.7.0 2.2.1
rubocop 1.59.0 1.60.2
So what do we have:
https://github.com/braindeaf/capistrano-puma - Capistrano Puma is for deploying my application but the latest version I need to support Puma 6.x removes some nice monit
support and it deprecates nginx
configs. All of which I use. This is just a fork to allow Puma 6.x to be installed rather than holding the dependency back. Perhaps I could added the nginx and monit support back into my own gem. Also I will need some way of maintaining anycable
systemd configs too so yes one day.
git@github.com:braindeaf/m3ta.git - I built a pretty functional meta tag management tool for my purposes. I haven’t published the latest update. I should. I should remember how the hell I did it last time. I will do that.
Then some other dependencies…
http-accept (1.7.0) is held back by rest-client (2.1.0) which is held back by ogpr (1.1.0) - This is just for grabbing Open Graph data from a url, I could do a pull request for OGPR to bring rest-client upto date. I’ll do that. https://github.com/hirakiuc/ogpr/pull/15
rubocop (~> 1.59.0) is being held back by standard (1.33.0) simples. I use Rubocop in some client projects and Juniper used standardrb which has it’s own standardised defaults which is fine. I can handle this. I’ll wait and upgrade if and when.
GIT
remote: git@github.com:braindeaf/m3ta.git
revision: 8f6b6f98941af3aee8fce4077f75a3896f3aef6a
branch: rl/refactor_into_view_component
specs:
m3ta (0.1.1)
hashie (= 5.0.0)
rails (~> 7.0, >= 7.0.4.1)
GIT
remote: https://github.com/braindeaf/capistrano-puma
revision: b4cc103868fb680841d523011598a270448f8a1b
branch: rl/support-puma6-in-5.2.0
specs:
capistrano3-puma (5.2.0)
capistrano (~> 3.7)
capistrano-bundler
puma (>= 4.0, < 7.0)
GEM
remote: https://rubygems.org/
specs:
---8<---
http-accept (1.7.0)
http-cookie (1.0.5)
domain_name (~> 0.5)
---8<---
ogpr (1.1.0)
nokogiri (~> 1.8)
rest-client (~> 2.1.0)
---8<---
rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
---8<---
rubocop (1.59.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.2.4)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.30.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
---8<---
standard (1.33.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.59.0)
standard-custom (~> 1.0.0)
standard-performance (~> 1.3)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
---8<---
DEPENDENCIES
---8<---
ogpr
---8<---
standardrb
---8<---
RUBY VERSION
ruby 3.3.0p0
BUNDLED WITH
2.5.5
Being out of date isn’t the end of the world, but it’s nice to think you’re on the cutting edge.
Since December I’ve some dev-ops which is not my usual gig. Today, just running round upgrading and rebooting staging servers. Alas MongoDB didn’t come back up on reboot. My immediate thought was that it wasn’t configured to start on start up
so let’s get that sorted and bring it up.
sudo systemctl enable mongod.service
sudo service mongod start
Gah…
$ mongo
MongoDB shell version v5.0.24
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Error: couldn't connect to server 127.0.0.1:27017, connection attempt failed: SocketException: Error connecting to 127.0.0.1:27017 :: caused by :: Connection refused :
connect@src/mongo/shell/mongo.js:372:17
@(connect):2:6
exception: connect failed
exiting with code 1
Ok, is the database corrupt or does Ubuntu think it’s already running and it isn’t or something?
$ sudo systemctl status mongod
● mongod.service - MongoDB Database Server
Loaded: loaded (/lib/systemd/system/mongod.service; enabled; vendor preset: enabled)
Active: failed (Result: exit-code) since Tue 2024-02-06 08:36:19 UTC; 43s ago
Docs: https://docs.mongodb.org/manual
Process: 2483 ExecStart=/usr/bin/mongod --config /etc/mongod.conf (code=exited, status=2)
Main PID: 2483 (code=exited, status=2)
Feb 06 08:36:19 178-79-140-122 systemd[1]: Started MongoDB Database Server.
Feb 06 08:36:19 178-79-140-122 mongod[2483]: Error opening config file: Permission denied
Feb 06 08:36:19 178-79-140-122 mongod[2483]: try '/usr/bin/mongod --help' for more information
Feb 06 08:36:19 178-79-140-122 systemd[1]: mongod.service: Main process exited, code=exited, status=2/INVALIDARGUMENT
Feb 06 08:36:19 178-79-140-122 systemd[1]: mongod.service: Failed with result 'exit-code'.
Ok, the /etc/mongod.conf
is not readable, it’s seemingly owned by root but something in a recent upgrade means that presumably the config is read by the mongodb
user rather than root. I assume.
sudo chown mongodb:mongodb /etc/mongod.conf
ls -la /etc/mongod.conf
-rw-r-x--- 1 mongodb mongodb 615 Dec 19 2013 /etc/mongod.conf
Cool. Let’s restart the service.
sudo service mongod start
And check the status
sudo systemctl status mongod
● mongod.service - MongoDB Database Server
Loaded: loaded (/lib/systemd/system/mongod.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2024-02-06 08:38:03 UTC; 4s ago
Docs: https://docs.mongodb.org/manual
Main PID: 2792 (mongod)
Memory: 182.8M
CGroup: /system.slice/mongod.service
└─2792 /usr/bin/mongod --config /etc/mongod.conf
Feb 06 08:38:03 178-79-140-122 systemd[1]: Started MongoDB Database Server.
Feb 06 08:38:03 178-79-140-122 mongod[2792]: {"t":{"$date":"2024-02-06T08:38:03.246Z"},"s":"I", "c":"CONTROL", "id":7484500, "ctx":"-","msg":"Environment variable MONG>
Cool. Beans and Sausages.
Running applications locally on your laptop is normally a doddle. Boot up a Rails app and point your browser at
http://localhost:3000
Easy peasy. But what if your application is not your average application. Suppose it’s multi-tenanted app with a hostname for each tenant and you want to try that locally. Well you could whack a load of entries in your /etc/hosts
/etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 demo.fossa-are-great.org
127.0.0.1 demo.fossa-are-ok.org
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
Then you could visit…
http://demo.fossa-are-great.org:3000/
http://demo.fossa-are-ok.org:3000/
…with ease and everything would be fine, even if port 3000 is now a little jarring. It is possible to remove the port if we ran our own version of Nginx running on port 80 and configured it to proxy onto our running application on port 3000. I’m going to go out on a limb and say there are will be a high proportion of developers that won’t know how to configure Nginx to do this and it’s a bit overkill anyway. However, puma-dev makes pretty light work of this.
Puma-dev: A fast, zero-config development server for macOS and Linux - https://github.com/puma/puma-dev
On macOS it’s pretty easy to set up with brew
brew install puma/puma/puma-dev
# Configure some DNS settings that have to be done as root
sudo puma-dev -setup
# Configure puma-dev to run in the background on ports 80 and 443 with the domain `.test`.
puma-dev -install
Then in order to configure our hostnames we create a file the ~/.puma-dev per hostname we want to support with the port of the running application.
~/.puma-dev/demo.fossa-are-great
3000
~/.puma-dev/demo.fossa-are-ok
3000
This time we have to visit our domain with the .test Top-Level Domain as Puma Dev, by default, supports that TLD. Puma-dev runs on port 80 and 443 as standard and so there is no need to supply a port.
http://demo.fossa-are-great.test/
http://demo.fossa-are-ok.test/
The way this works is really neat and I’m a bit of a fan to be fair. Hostnames on macOS and Linux flavours uses /etc/resolver/. Puma creates a file /etc/resolver/test
to indicate that name resolution for .test domains should use a local nameserver running on port 9253 to resolve hostname blah.test
# Generated by puma-dev
nameserver 127.0.0.1
port 9253
In fact let’s try it
dig blah.test @127.0.0.1 -p 9253
; <<>> DiG 9.10.6 <<>> blah.test @127.0.0.1 -p 9253
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2962
;; flags: qr rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available
;; QUESTION SECTION:
;blah.test. IN A
;; ANSWER SECTION:
blah.test. 0 IN A 127.0.0.1
;; Query time: 0 msec
;; SERVER: 127.0.0.1#9253(127.0.0.1)
;; WHEN: Mon Feb 05 15:42:26 GMT 2024
;; MSG SIZE rcvd: 52
Yep, this returns and resolves our domain to 127.0.0.1, however visiting http://blah.test will result in the following because we haven’t configured puma-dev to point at an internal port.
unknown app
This is pretty neat huh? Well that’s not the end of the story. What if we wanted to use https://demo.fossa-are-great.test. Well that just works out of the box. Like I said it also runs on port 443. Puma manages the certificate authority for the .test domain locally and adds the certificate and key to the local keychain so now we have valid SSL. Magic.
% ls -la ~/Library/Application\ Support/io.puma.dev
total 16
drwx------ 4 rl staff 128 16 Jan 09:03 .
drwx------+ 62 rl staff 1984 29 Jan 02:06 ..
-rw-r--r-- 1 rl staff 1208 16 Jan 09:03 cert.pem
-rw------- 1 rl staff 1679 16 Jan 09:03 key.pem
I’ve been using this setup for years now and it has enabled us to run multiple applications locally and fully test running up to 5 different multi-tenanted Rails apps logging users in with the same cookie domain as an extreme example.
https://app1.blah.something.test
https://app1.club.something.test
https://app2.blah.something.test
https://app2.club.something.test
https://app3.blah.something.test
https://app3.club.something.test
I have only ever found one issue with this setup and that was today.
Today’s issue involved building PDFs with Grover that had missing styles and as it turned out the this was only over SSL so there was an issue with the certificate which was fixed by re-configuring Puma-dev to presumably re-add the certificate to the keychain.
puma-dev -uninstall
sudo puma-dev -setup
puma-dev -install
puma-dev -stop
Thumbs up for puma-dev
This year has been a weird one. And life takes a turn.
At the end of last year, after 4 years, I decided to move on from SARD JV Limited they are lovely people and the team are great and we still talk but for varying personal reasons, it could not continue and I set out to seek my fortune elsewhere.
I landed with Builder.ai in January of this year and realised pretty quickly that the role I had taken was not for me. I’m not afraid of meetings, planning, mentoring, code reviews, far from it. But when I realised that I wasn’t writing any code myself that became a problem. With SARD JV I was in the heart of everything, guiding the development of the application and introducing new ways of restructuring code, adding in performance gains the whole package, I had code coming out of my ears and I was waking up thinking about code. Without being in the thick of it I was in a place where I wasn’t happy. This is the first time I felt like I’d made the wrong choice and it was wrong to continue for myself and for Builder.ai so I made a quick exit.
In April I joined Juniper Education and have for the past six months been working as a Senior Developer in an exceptional team on a pretty complex Assessment tracking system for UK schools. Plenty of performance problems to tackle and interesting UIs. We were on the verge of launching the product in the coming months. Sadly, however, the business has decided not to continue with Ruby. I mean I think Ruby is pretty great, but… that’s not our decision. That means a medium size team of developers were released in the wild on Tuesday of this week.
If only there was a startup that needed 5 to 10 developers to that were already comfortable working with one another to start on a new product. Wishful thinking I know.
So for now I’m on the hunt for work myself. I have all of my possessions wrapped in a hankerchief on the end of a stick and I am off to the end of our driveway to seek my fortune. That might be a lie.
So here goes “…I’m a seasoned but not salty Senior Software Engineer from Brighton, UK who’s been working with Ruby/Rails (remotely) for the past 15 years. I’m looking for a fully remote role that is hands-on focusing on code rather than people management although I love working with other developers and wish to continue code review and mentoring. I want to make a positive impact on the team I land in and inject my experience in a new project/adventure…”.
If you have a quest that needs a Tank, I’m available and ready to work with you. Feel free to take a look at my CV and any feedback positive or negative is very welcome.
It appears to be the wrong time of year to be looking for a new Ruby/Rails gig. But unfortunately my brain isn't ready to hibernate for the Winter. So if you have hard problems, horrible upgrades or just need a reliable, competent, conscientious Senior Ruby Dev... #ruby #rails pic.twitter.com/gKEJSfT5J1
— RobL (@braindeaf) November 27, 2023
Syntax Highlighting is important, it just makes code all warm and fluffy and more engaging. I currently use HighlightJS in my Rails 7 app.
./bin/importmap pin highlightjs
Which adds the following to `config/importmap.rb`
pin "highlightjs", to: "https://ga.jspm.io/npm:highlight.js@11.4.0/es/index.js"
Over the years I have used a number of different ways to organise my code examples in blog posts. They are a mixture of RedCloth and HTML and I used to used to use CodeRay and then SyntaxHighlighter to render code blocks nicely.
<x-source:ruby>
class Cat; end
</x-source>
but eventually settled on pre > code and HighlightJS<pre>
<code>
class Rabbit; end
</code>
</pre>
As an aside I never updated the old blog posts but created a renderer to update the body of the posts to comply to the new format, and even ensure that I could embed my pre > code html in examples too by escaping the contents of every pre > code element.
# frozen_string_literal: true
class Content
def initialize(string)
@string = string
end
def render
::Haml::Helpers.preserve(segments.join('')).html_safe
end
private
def string
@string.gsub(/<source(:[a-z]+)?>/, '<pre><code>')
.gsub(%r{</code></pre>}, '</code></pre>')
.gsub(%r{<code>([\s])+}, "<code data-controller='highlight'>")
end
def segments
string.split(/(<pre><code>.*?<\/code><\/pre>)/m).map do |s|
if s =~ /<pre><code>.*?<\/code><\/pre>/m
s.gsub(/<pre><code>(.*)?<\/code><\/pre>/m) { "<pre><code data-controller='highlight'>#{CGI::escapeHTML($1)}</code></pre>" }
else
RedCloth.new(s).to_html
end
end
end
end
I could then use just a turbo:load EventListener to apply the highlight the code blocks.
app/javascript/application.js
import "@hotwired/turbo-rails"
import "controllers"
// Turbo.session.drive = true
import HighlightJS from "highlightjs"
window.HighlightJS = HighlightJS
document.addEventListener('turbo:load', (event) => {
console.log('turbo:load')
document.querySelectorAll('pre code').forEach((el) => {
HighlightJS.highlightElement(el);
});
});
However, this doesn’t work with Turbo-Frame, the `turbo:load` callback is only run on full page loads and I want to use them in Turbo-Frame(s). I am sure there is another callback I could run but honestly I’d prefer to use a Stimulus Controller and know that it will always trigger when the DOM changes and my code block is added to the DOM.
app/javascript/controllers/highlight_controller.js
import { Controller } from "@hotwired/stimulus"
import HighlightJS from "highlightjs"
export default class extends Controller {
connect() {
HighlightJS.highlightElement(this.element)
}
}
With code blocks looking like
<pre>
<code data-controller="highlight">
class Rabbit; end
</code>
</pre>
This seemed to work in development but failed in production. AS it turns out I was caching the content of every blog post, so it wouldn’t need to re-render every
Rails.cache.clear
For bonus points I’ve added support for triple back-tick ```language delimited blocks for code.
# frozen_string_literal: true
class Content
def initialize(string)
@string = string
end
def render
::Haml::Helpers.preserve(segments.join('')).html_safe
# rescue => e
# 'BROKEN'
end
private
def string
@string.gsub(/<source(:[a-z]+)?>/, '<pre><code>')
.gsub(%r{</code></pre>}, '</code></pre>')
end
def segments
rewritten = string.split(/(```[a-z]+?\n.*?```)/m).map do |segment|
if segment =~ /```([a-z]+)?\n.*?```/m
segment.gsub(/```([a-z]+)?\n(.*)?```/m) { "<pre><code data-controller='highlight'>#{CGI::escapeHTML($2)}</code></pre>" }
else
segment
end
end.join('')
rewritten.split(/(<pre><code[^>]+?>.*?<\/code><\/pre>)/m).map do |s|
if s =~ /<pre><code>.*?<\/code><\/pre>/m
s.gsub(/<pre><code>(.*)?<\/code><\/pre>/m) { "<pre><code data-controller='highlight'>#{CGI::escapeHTML($1)}</code></pre>" }
elsif s =~ /<pre><code data-controller='highlight'>.*?<\/code><\/pre>/m
s
else
RedCloth.new(s).to_html
end
end
end
end
However, I should just bulk update the content of the blog bodies to not use the old tag syntax I implemented years ago anyway.
Fun Thursday.
Most people’s first project in a language / framework is a simple ToDo example or a blog. I guess I’m in the same situation. However, my first Ruby project was a blog and I’ve been dumping code snippets, findings and moans on that for 10 years now. So my first Elixir project should really be to port my Rails blog to Phoenix. Here’s some things I learned along the way.