Compare commits
64 Commits
122febceb4
...
29db3ce86a
| Author | SHA1 | Date | |
|---|---|---|---|
| 29db3ce86a | |||
| 2d89475f0b | |||
| f71fbdafac | |||
| 90b16dad40 | |||
| 05b9308c1b | |||
| ca8e6824ea | |||
| a0f2505728 | |||
| 3651679c86 | |||
| b0bffe5d72 | |||
| 86446de9a8 | |||
| a63b8c6a0b | |||
| ef681069c7 | |||
| 5797bc85bf | |||
| 4c829133a3 | |||
| 2328446520 | |||
| af4adc0fc8 | |||
| f61a00d292 | |||
| 7d4ce6f073 | |||
| db5b029f78 | |||
| e8754afca2 | |||
| c179adccb2 | |||
| caed7bbfdf | |||
| ff9df11a15 | |||
| 5396812290 | |||
| 9996a47d0c | |||
| abd249030b | |||
| 3c132ca4dc | |||
| f219908d67 | |||
| 5e371cf734 | |||
| 76901304c4 | |||
| 83c25e328f | |||
| ccd7140d6d | |||
| 0db52b89f0 | |||
| 93cda87bbd | |||
| 35ef330df8 | |||
| 348b76aa04 | |||
| df748183ab | |||
| e470dfc258 | |||
| 56e2c037b5 | |||
| c5c4167361 | |||
| 2f70f87f41 | |||
| 0519d155fa | |||
| 6d801d0f0e | |||
| fd21d77526 | |||
| 5b927900ae | |||
| 5fcb2bcc83 | |||
| 6dd4caf947 | |||
| 506d1adfcc | |||
| e506f7c2e6 | |||
| 574b2d6646 | |||
| b0adbd4419 | |||
| 8125081177 | |||
| 229339dc17 | |||
| 99b54df287 | |||
| 70689047ca | |||
| b4be3ed2a6 | |||
| 8e814917b6 | |||
| a8686d9169 | |||
| 830f63641b | |||
| 3e0def4305 | |||
| 722a304023 | |||
| 423fa1ed26 | |||
| 6cdc0ad1e6 | |||
| 65b3169d98 |
@@ -6,9 +6,9 @@ Structure:
|
|||||||
|
|
||||||
```
|
```
|
||||||
/
|
/
|
||||||
├── capsule My Gemini capsule
|
├── gemini My Gemini capsule
|
||||||
├── smtp A local SMTP server
|
├── smtp A local SMTP server
|
||||||
└── website My Website
|
└── http My Website
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running with Podman
|
## Running with Podman
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ Some authors have managed to recognise the ambiguity of the term. Both King et a
|
|||||||
|
|
||||||
So it seems that although many academics have treated the term ‘hallucination’ as jargon, in actual fact, there is no widely agreed specific meaning of the word.
|
So it seems that although many academics have treated the term ‘hallucination’ as jargon, in actual fact, there is no widely agreed specific meaning of the word.
|
||||||
|
|
||||||
<hr>
|
---
|
||||||
|
|
||||||
I will slide in here with a quick side note. While some authors treated ‘hallucination’ as jargon for unfaithfulness or unfactuality, other authors contemporaneously managed to talk about these topics without using the word ‘hallucination’ at all. Here are some examples:
|
I will slide in here with a quick side note. While some authors treated ‘hallucination’ as jargon for unfaithfulness or unfactuality, other authors contemporaneously managed to talk about these topics without using the word ‘hallucination’ at all. Here are some examples:
|
||||||
|
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
# "381: how the church as we know it was made"
|
|
||||||
|
|
||||||
The church which defines our world now is in a significant way the one which emerged out of sixty years of controversy from the Council of Constantinople in 381. I've been charting what happened, why, and the ongoing legacy.
|
|
||||||
|
|
||||||
Published on: 5 Oct 2025
|
|
||||||
|
|
||||||
Athanasius defined the fourth century. Not that he was a god, or even a king, or that he always got his way. But he wrote the history books. His tale of an epic battle fought tooth-and-nail between Arian heretics and him and his loyal allies has come to be the standard account of how, over the course of the fourth century, the Church redefined what orthodoxy means and how it is declared and identified.
|
|
||||||
|
|
||||||
The result was the Nicene Creed. It had been first written for a very particular polemical purpose in 325, but later found itself the centre of a strange theological revival, and was finally revised in a council at Constantinople in 381. In so doing, the bishops assembled a recognisable 'Nicene' tradition which is still one of the defining features of planet Earth.
|
|
||||||
|
|
||||||
For better and for worse, the church as we know it has a capacity both for great humility, faith and submission to the mystery of God, but it also has a capacity for great intolerance. This is the church created in 381.
|
|
||||||
|
|
||||||
To understand the church as we know it today, then, we need to understand the complex, confusing journey from 325 to 381.
|
|
||||||
|
|
||||||
Athanasius' chronicle of that journey is temptingly simple. The only problem with it is that it isn't true. Indeed, his 'history' was never meant to function as an all-encompassing narrative of Church history, to be read for centuries ever after. His accounts function as polemics, meant to cajole, condemn and persuade his readers in his own time of his vision for their future.
|
|
||||||
|
|
||||||
Nevertheless, whatever Athanasius' real significance in how his times unfolded, his witness is important. He fully inhabited his times, often in the middle of the fray. Whether or not we buy Athanasius' portrayal of himself as fighting the good fight, he was certainly a fighter. By looking through his eyes, then, we can get a perspective on how the Church as we know it came to be.
|
|
||||||
|
|
||||||
So it makes sense to start with him. As a young priest in his native Alexandria, he became tangled up in a controversy which would come to define his career. A strong-minded and fearless young priest had begun to preach. His name was Arius.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
According to the Egyptian tradition, Alexander, the bishop of Alexandria, was the nineteenth in a direct line of succession from Mark the Evangelist himself. With a great deal of justice, he would have regarded himself as one of the most important Christian leaders in the world, and at least the equal of the bishop of Rome.
|
|
||||||
|
|
||||||
Small wonder, then, that the insubordination that plagued his diocese bothered him. First Erescentius had started a schism, disputing the rule he used for calculating the date of Easter.
|
|
||||||
|
|
||||||
Then there was Meletius. During the persecution under the Roman emperor Diocletian, Meletius had already rubbed a few people the wrong way: while other bishops were in hiding or in prison, he took the initiative to resolve problems and ordain priests without properly consulting the absent bishops' representatives. Perhaps it was intended kindly: it was seen as meddling. Now Meletius accused Alexander of being too soft on Christians who had caved into the threat of torture and made sacrifices to the pagan cults. When he decided Alexander was never going to match his high rigorist standards, he broke away, too.
|
|
||||||
|
|
||||||
Alexander must have longed for the relatively good order of the Greek and Roman churches, where bickering subordinates were generally willing to let their bishop have the last say. The throne of St Mark was in trouble. If Christ's body wasn't to get chopped up any more than it already was, he needed to establish his personal authority.
|
|
||||||
|
|
||||||
This was the context in which Arius, a young firebrand priest, steps onto stage right. He surely knew his own bishop's teaching: God is one substance and one essence, unchangeable, indivisible. Christ his Son is in every way God: God from God, light from light, true God from true God, eternally begotten of the Father before all ages. How else could Christ, by adopting human flesh, mediate the transcendent God to fallen humanity?
|
|
||||||
|
|
||||||
But Arius didn't like this one bit. If God is unchangeable, how could he adopt flesh? That suggests he was not flesh, and then became flesh. And in any case, if the martyrs were right to give up their lives to know God, he must have the perfect, uncompromising transcendence which the martyrs so admired. But how can God adopt flesh, never mind suffer and die on a cross, without compromising that transcendence? Something had to give. For Arius, the solution was to modify the relationship between the Father and the Son.
|
|
||||||
|
|
||||||
Arius accepted that Christ had to be in some sense divine, in order to mediate God to humanity. But he denied that he was quite as much God as God is. He has something like his Father's essence, not in a co-equal way, but rather in a derivative way. This makes sense of Father-Son language, which suggests the Father came first, and the Son came next, a derivative of the Father. So the Son is God from God, but not true God from true God. The Son was begotten in time, and is not eternal: only God the Father himself is eternal.
|
|
||||||
|
|
||||||
At another time in another place, Arius might have passed for a creative, independent thinker without much notice. But Arius was directly contradicting Alexander just as the latter was desperate to assert his authority. It got ugly.
|
|
||||||
|
|
||||||
Alexander called a council of local bishops in about 320. The council condemned Arius and removed him from his post as priest. In response, Arius went on the campaign trail, visiting bishops in Palestine and Asia Minor who he thought would be sympathetic to his theology. Shortly afterwards, he returned to Alexandria, triumphantly brandishing vindications from two councils, one in Jerusalem and one in Bithynia. He wasn't going to make it easy for Alexander.
|
|
||||||
|
|
||||||
Luckily for Alexander, the Emperor Constantine had just united the eastern and western halves of the Empire. He had famously converted to Christianity after seeing the sign of the cross at the Battle of Milvian Bridge in 312, and saw the bishops as means towards his mission of uniting the Empire under one government and one God. Constantine had been made aware of the dispute between Arius and Alexander, and he didn't want schisms in the church any more than Alexander did.
|
|
||||||
|
|
||||||
He called a council in his own palace in Nicaea, paying the travel expenses and hotel bills of all the bishops in attendance. For those bishops, many carrying the scars of torture they had endured under Diocletian, it must have been a bewildering experience. Alexander was in attendance. His secretary was Athanasius.
|
|
||||||
|
|
||||||
In 325, the council condemned Arius. To avoid anyone else following in his path, they produced a statement of faith, designed to exclude Arius' teaching, no matter who taught it. This statement of faith is now known as the Nicene Creed.
|
|
||||||
|
|
||||||
The council also fixed the date of Easter to boot. Alexander must have been relieved.
|
|
||||||
|
|
||||||
You might have thought that would have been the end for Arius. In fact, Constantine engineered his re-admittance into the church as soon as 328. Arius died in peace in 336. Constantine's mission wasn't to purge the church, but to unite the church. As long as all sides worshipped God and could live in peace, he wanted as many people as possible included. His mission was unity, not uniformity.
|
|
||||||
|
|
||||||
Bishops like Eusebius of Caesarea in Syria got this. He had been provisionally excommunicated on suspicion of Arianism in 325, but was reconciled at Nicaea given the chance to explain himself and sign up to the Nicene Creed. No sooner had he done this, however, than he had started explaining to the faithful back home how they could carry on believing that the Son was not really eternal, even as the Creed was designed to exclude exactly such a claim. While Eusebius might seem duplicitous, at the time, this was exactly the kind of tolerant pragmatism that Constantine asked of the bishops: as long as they didn't cause more out-and-out conflict.
|
|
||||||
|
|
||||||
Alexander didn't have long to enjoy the peace of Nicaea. He died just a few years afterward in 328. The throne of St Mark passed to Athanasius.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
The peace didn't last long. Just as Athanasius was donning his mitre, Eusebius was plotting against Eustathius the bishop of Antioch, and engineered his deposition. In his defence, Eusebius accused Eustathius of the long-condemned heresy, Sabellianism. Then in 335, he followed up by deposing Marcellus, the bishop of Ancyra, at a council in Tyre.
|
|
||||||
|
|
||||||
To defend his action, he wrote _Against Marcellus_, in which he accused Marcellus of being a Sabellian, too. Sabellius' heresy was (to borrow a modern term) modalism, the view that 'Father', 'Son' and 'Spirit' are mere titles, aspects, 'modes' of God, not in any real way distinct. He also accused Marcellus of adoptionism, another agreed heresy. Marcellus taught that the Son only became an aspect of the divine nature at the Incarnation, and that in the last day, Christ would hand over his kingdom to his Father.
|
|
||||||
|
|
||||||
This action would cast a long shadow over the next half-century. Time and again, bishops allied to Eusebius' way of thinking, or 'Eusebians', would re-affirm their opposition to that 'heretic' Marcellus and his 'Sabellianism'. This is a crucial dynamic for understanding where theological factions drew up their battle lines, and for what compromises were needed in order to get to 381.
|
|
||||||
|
|
||||||
Even the bishop of Alexandria wasn't immune from Eusebius' purge. Athanasius had vigorously defended his ally, Marcellus, at the council of Tyre in 335. Eusebius set about plotting his downfall. He dug up dirt. He accused Athanasius of using threats and bribes to get himself elected, and sending goons to beat up his political opponents. Once he'd found evidence of Athanasius meddling with the crucial Egyptian grain export that kept Rome fed, he had the emperor on side. Constantine convened a meeting in 336 and exiled him to the German frontier.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Or at least, that's how Athanasius tells it. Athanasius loves a plot: at the time, alleging a conspiracy was a classic rhetorical technique for painting your enemies as heretics.
|
|
||||||
|
|
||||||
Eusebius was no stranger to rhetoric himself, and it's to his 337 best-seller, the _Life of Constantine_, that we owe our standard account of Constantine's reign. He regarded Empire and Church as allies in a joint mission, to unite the world under one government and one faith. To him, someone like Athanasius, constitutionally incapable of tolerating anyone who disagreed with him and willing to use gangster tactics to get his way, was a threat to this divine mission.
|
|
||||||
|
|
||||||
It's worth remembering that after Constantine died, Athanasius would be re-exiled by four more Roman emperors. In his lifetime, only Julian failed to exile Athanasius, and him only perhaps because he didn't have time in his whirlwind twenty-month reign. We also can't be sure how much influence Eusebius actually had in the expulsion of Athanasius and his allies: it coheres well enough with the emperor's anti-sectarian agenda that it might have happened with or without Eusebius' involvement.
|
|
||||||
|
|
||||||
Perhaps Athanasius was a brute. Still, the Roman Catholic Church manages to venerate both Eusebius and Athanasius as saints. This may seem like a contradiction. But perhaps an ability to tolerate contradiction is precisely the legacy of 381.
|
|
||||||
|
|
||||||
But we're not there yet. By 335, Eusebius had engineered the exile of Eustathius, Marcellus, and Athanasius. After Constantine died, he had to do it all over again, but by 339, he had persuaded his successor, Constantius, to re-assert his father's exiles of the three men. With the Empire once again split, Athanasius and Marcellus headed to Rome to re-group and re-think.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
From Rome, Athanasius and Marcellus were safe for now from Eusebius' clutches, but also relatively impotent. In this period of exile in the 340s, in an effort to claw back his reputation, Athanasius developed the polemic which still defines the standard history of the fourth century. He invented a cunning label for Eusebius and his cronies: he called them 'Arians'.
|
|
||||||
|
|
||||||
Eusebius rejected the label as ridiculous. Arius had been reconciled, and more to the point, had died in 336. For that matter, why would a bishop follow the teaching of a mere priest? Not only that, but the label ignored significant differences between Arius' and Eusebius' teaching. His verdict was clear: the label 'Arian' is a baseless slur, with no other purpose than to tar his reputation as a heretic.
|
|
||||||
|
|
||||||
He was right, of course. But like it or not, Athanasius' theory of an Arian conspiracy began to win adherents, not least Julian, the bishop of Rome. Julian called a council to exonerate Athanasius and Marcellus. When the Greeks refused to turn up, he called a local council anyway and vindicated the two men. In the face of Greek obstinacy, Julian wrote east, pleading the bishops to take the 'Arian' threat seriously.
|
|
||||||
|
|
||||||
In response, the easterners held a council in Antioch in 341, agreeing four creeds which powerfully condemned Marcellus' teaching, including the influential Dedication Creed. This includes assertions that Father, Son and Spirit are 'three in subsistence, one in agreement', that the Son was generated before time began, against Marcellus' teaching that the Father, Son and Spirit are aspects of God without division in subsistence, and that there only came to be a divine Son at his incarnation. They explicitly condemned Arius, Sabellius and Marcellus.
|
|
||||||
|
|
||||||
So the divisions grew deeper. Without an emperor to compel the bishops to come together, there may not have been much chance of a rapprochement. But even if there were to be such an emperor, who's to say that their settlement would have satisfied the bishops?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Meanwhile, in the 340s and through the 350s, two further theological movements gathered steam: the homoians and the heterousians.
|
|
||||||
|
|
||||||
The homoians, perhaps tired of the squabbles between the Athanasian and Eusebian factions, determined to sidestep their petty debates altogether.
|
|
||||||
|
|
||||||
A key term of the theological disagreement was 'essence' or 'ousia'. Athanasius, in his lifelong battle to make sure Arius stayed dead, insisted that Father, Son and Spirit shared the same ousia. In contrast, Eusebius, with his anti-Sabellian polemic, needed to assert the real distinction between Father, Son and Spirit, and so asserted that each had a separate ousia. So the difference can be summed up as a counting problem. How many divine ousias are there? One or three?
|
|
||||||
|
|
||||||
The homoians claimed that both sides were mistaken, simply because they used the word 'ousia'. There is no mention of ousia in Scripture, so, they claimed, we have no basis for asserting it of God one way or the other. All we can truly say is that Father, Son and Spirit are distinct but somehow alike. Whereof we cannot speak, there must we remain silent.
|
|
||||||
|
|
||||||
This might have worked as a way forward, except that the heterousians provoked such a strong reaction that 'ousia'-talk was needed to refute them. Aetius, and his followed Eunomius, argued that since God is simple, and all generate things are divided, it follows that God is ingenerate. But the Son is generate: therefore Father and Son must be altogether unalike. They expressed this by saying that Father and Son are unlike in ousia. This teaching was swiftly branded 'neo-Arian', provoking a strong reaction. To counter the heterousian teaching, their opponents were forced to fight on their terms, and that meant using 'ousia'-talk.
|
|
||||||
|
|
||||||
Thus enters Basil of Caesarea. He argued that if we abandon 'ousia'-talk, we will have no way of saying that the Father and Son have anything in common at all, which makes a nonsense of the idea that the Son brings humanity knowledge of his Father. Without like essence, they might as well be two completely different Gods. Therefore we have to say at least that they have like essence -- 'homoiousia'. But without direct access to perfect knowledge of the invisible God, we're not in a position to judge that they have exactly the same essence, so he stopped short of agreeing with the 'homoousia' of the Nicene Creed which Athanasius so treasured.
|
|
||||||
|
|
||||||
Seeing the opportunity to make common cause against the homoians, Athanasius started to soften. He wrote an extremely charitable commentary on Basil's theology which emphasised their similarities and papered over their differences. Athanasius recognised that both he and Basil wanted to assert the unity of God while still preserving distinctions between Father, Son and Spirit. The two began to campaign against the homoian movement.
|
|
||||||
|
|
||||||
But Basil got there too late. In 359, the emperor Constantine II called a council in Constantinople, and in 360 it issued a homoian creed with full imperial backing. Any campaign against the homoians would have to take place sub rosa.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
In Athanasius' and Basil's long, slow campaign against homoianism, their weapon of choice was surprising: they dusted off the Nicene Creed of 325. Athanasius argued, against the homoians, that 'ousia'-talk, although not directly Scriptural, was essential in order to draw out the consequences of Scripture while ruling out Arian mis-interpretations.
|
|
||||||
|
|
||||||
Thus Nicaea, conceived as a one-off meant to clean up the Arian controversy, found a new life as the anti-homoian movement -- or perhaps you could call it the Nicene revival? -- rallied around it.
|
|
||||||
|
|
||||||
As the movement progressed, the formerly disagreeing bishops found ways to come together. An essential move was that made in Athanasius' _Antiochene Tome_ of 362. In it, he relented on his long opposition to there being three 'hypostases' or 'substances' in the Godhead.
|
|
||||||
|
|
||||||
'Hypostasis' had for a long time been used interchangeably with 'ousia'. However, Athanasius claimed that perhaps God could have three hypostases, but only one ousia, at the same time. In so doing, he wedged apart a sharp technical distinction between 'hypostasis' and 'ousia' which previously wouldn't have made sense. Logical or not, it enabled the Nicene revival to have its cake and eat it. God is both one in ousia, protecting against Arianism, and three in hypostasis, protecting against Sabellianism.
|
|
||||||
|
|
||||||
So the Nicene revival gained a new superpower: the power to use formerly synonymous terms to assert contradictions without blushing. This power to accept apparent contradiction as part of the unknowable mystery of God is perhaps the most important legacy of the period. Arguably, the church has been at its best when it has put aside the need to know everything, and embraced this spirit of tolerance, humility and faith.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For much of the 360s and 370s, the homoian emperor Valens had ruled over the eastern part of the Empire, while his big brother, Valentinian, ruled the west. In the late 370s, Valentinian and then Valens died within quick succession of each other. Valentinian's twenty-year-old son, Gratian, was left to clear up the mess. In 379, Gratian delegated rule of the east to Theodosius, who was to implement a decisively different religious policy than his predecessor, Valens.
|
|
||||||
|
|
||||||
In 380, Theodosius issued an edict, saying that only those who agreed to the homoousios clause of the Nicene Creed could be considered 'catholic' Christians. The message was clear: the homoians were out, and the Nicenes were in.
|
|
||||||
|
|
||||||
In 381, he called a council to Constantinople, and it (probably) issued the revision of the 325 creed which is still used in various versions in all the world's largest Christian denominations. There would be no more revisions, and it would become, then as now, compulsory reading for all those preparing to don vestments.
|
|
||||||
|
|
||||||
One question is, why did the 381 creed differ in the ways it did from 325? Many of the differences, including the much-enlarged section on the Son, seem to have little controversial content: nobody was disputing that Jesus was born of the Virgin Mary, for example, though she makes her first appearance in the Creed in the 381 version. Some historians think this suggests that the 381 was based on a similar, but distinct creed from 325. This seems unlikely to me, given that about half the creed is in verbatim agreement with 325.
|
|
||||||
|
|
||||||
However, a couple of edits stand out. There are some clear signs of anti-Marcellianism: 'his \[the Son's] kingdom shall have no end', the Son is begotten of the Father 'before all ages'. Perhaps a clear emphasis on the eternal relationship between the Son and the Father was part of the diplomacy needed to get the Eusebian faction on-side.
|
|
||||||
|
|
||||||
The new details on the Holy Spirit are interesting too. They suggest a delicate compromise. Some bishops were reluctant to suppose that the Father and the Spirit have the same essence. On the other hand, others reckoned that they must share the same essence, given that they are equally deserving of worship. Thus the creed does not have a 'homoousios' clause for the Spirit, but does assert that the Spirit 'together with the Father and with the Son is worshipped and glorified'. With a spoonful of humility, both sides can be satisfied with that.
|
|
||||||
|
|
||||||
The revised Nicene Creed was the focus point, the distillation of a growing theological movement, formed by the various anti-homoian bishops finding a way to keep true to their own convictions while respecting each other's red lines.
|
|
||||||
|
|
||||||
As a result of the context of 325, Athanasius' relentless anti-Arian polemic which kept that movement alive, and the 'neo-Arian' heterousian movement, the new Nicene tradition insisted on the full co-equal divinity of Father, Son and Holy Spirit. This doctrine ensures Nicenes can affirm that Christ mediates true knowledge of the transcendent Godhead to humanity: the one who was born of Mary, suffered and died on the cross, was raised from the dead and ascended into heaven was true God from true God, of the same essence as his Father.
|
|
||||||
|
|
||||||
To satisfy the Eusebian strain, which defined itself by opposition to Marcellus, the Nicene tradition included a commitment to a robust distinction between Father, Son and Spirit, and to the eternity of the Son: begotten of the Father before all ages, his kingdom shall have no end. As a result, Nicenes inherited a way of thinking about God's action in the world, as instrinsically co-operative without being divided.
|
|
||||||
|
|
||||||
The biggest change between 325 and 381 was not the text, but what the text is used for. In 325, the Creed functioned to condemn Arius in order to heal the divisions his teachings had caused. In its second life, the Creed found an altogether new purpose: to serve as a common statement of orthodox faith. It started life as a way to define who was out. It ended up defining who was in.
|
|
||||||
|
|
||||||
Where was Athanasius? Consider that when Athanasius was appointed bishop in 328, he was relatively young for a bishop at thirty-five. That means that in 381, he would have been the ripe old age of eighty-eight. In fact, he didn't make it that far: he died in peace in the countryside outside his native Alexandria in 373. If he had seen the outcome of 381, he might have regarded his life project complete. Perhaps he knew that with the new generation of bishops, the tide was turning for good, and died in peace. Perhaps not. Either way, his compromises, and his beloved homoousios, have left a permanent mark on the church.
|
|
||||||
|
|
||||||
This is the legacy of 381. It is two-faced: any common statement of faith can be used to exclude. Indeed, in the late fourth century, both non-Nicene Christians and pagans found themselves the victims of increasing state-backed sectarian violence.
|
|
||||||
|
|
||||||
However, 381 also bears witness to the power of humility and faith. Once we stop grasping at perfect knowledge we cannot attain, we can begin to appreciate the mystery of God. This is one legacy I hope we can carry forward into our century.
|
|
||||||
|
|
||||||
+7
-7
@@ -1,17 +1,17 @@
|
|||||||
services:
|
services:
|
||||||
capsule:
|
gemini:
|
||||||
image: git.joeac.net/joeac/joeac.net-capsule
|
image: git.joeac.net/joeac/joeac.net-gemini
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: capsule.Dockerfile
|
dockerfile: gemini.Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "1965:1965"
|
- "1965:1965"
|
||||||
|
|
||||||
website:
|
http:
|
||||||
image: git.joeac.net/joeac/joeac.net-website
|
image: git.joeac.net/joeac/joeac.net-http
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: website.Dockerfile
|
dockerfile: http.Dockerfile
|
||||||
args:
|
args:
|
||||||
LOCAL_SMTP_HOST: smtp
|
LOCAL_SMTP_HOST: smtp
|
||||||
LOCAL_SMTP_PORT: 2500
|
LOCAL_SMTP_PORT: 2500
|
||||||
@@ -19,7 +19,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- "8000:4321"
|
- "8080:8080"
|
||||||
|
|
||||||
smtp:
|
smtp:
|
||||||
image: git.joeac.net/joeac/joeac.net-smtp
|
image: git.joeac.net/joeac/joeac.net-smtp
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ RUN wget -O - https://git.sr.ht/~nytpu/comitium/archive/v1.8.2.tar.gz | tar -xz
|
|||||||
FROM alpine:3.23 AS final
|
FROM alpine:3.23 AS final
|
||||||
RUN mkdir -p /var/app/content
|
RUN mkdir -p /var/app/content
|
||||||
WORKDIR /var/app
|
WORKDIR /var/app
|
||||||
COPY capsule/.certificates .certificates
|
COPY gemini/.certificates .certificates
|
||||||
RUN crontab -l > crontab.tmp \
|
RUN crontab -l > crontab.tmp \
|
||||||
&& echo "0 */6 * * * /usr/local/bin/comitium refresh --data /var/app/comitium-data" >> crontab.tmp \
|
&& echo "0 */6 * * * /usr/local/bin/comitium refresh --data /var/app/comitium-data" >> crontab.tmp \
|
||||||
&& crontab crontab.tmp \
|
&& crontab crontab.tmp \
|
||||||
@@ -31,7 +31,7 @@ RUN rc-update add crond
|
|||||||
COPY --from=agate /root/.cargo/bin/agate /usr/local/bin/agate
|
COPY --from=agate /root/.cargo/bin/agate /usr/local/bin/agate
|
||||||
COPY --from=comitium /usr/local/bin/comitium /usr/local/bin/comitium
|
COPY --from=comitium /usr/local/bin/comitium /usr/local/bin/comitium
|
||||||
|
|
||||||
COPY capsule .
|
COPY gemini .
|
||||||
COPY common /var/common
|
COPY common /var/common
|
||||||
RUN make
|
RUN make
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM nginx:1.30.3-alpine3.23
|
||||||
|
RUN apk --update add build-base make
|
||||||
|
RUN make --directory=http --makefile=http/Makefile
|
||||||
|
COPY http/php /usr/share/nginx/php
|
||||||
|
COPY http/out /usr/share/nginx/html
|
||||||
+4
-2
@@ -1,4 +1,6 @@
|
|||||||
out
|
out
|
||||||
vendor
|
vendor
|
||||||
src/microlog
|
src/microlog/*.microlog.upphtml
|
||||||
src/blog
|
src/microlog/*.microlog.upphtml.args
|
||||||
|
src/blog/*.blog.upphtml
|
||||||
|
src/blog/*.blog.upphtml.args
|
||||||
|
|||||||
+93
-77
@@ -1,97 +1,100 @@
|
|||||||
all: microlog blog public out
|
include config.mk
|
||||||
|
|
||||||
.PHONY: prune
|
|
||||||
prune: prune_microlog prune_blog prune_out
|
|
||||||
|
|
||||||
.SILENT: prune_out
|
#############
|
||||||
.PHONY: prune_out
|
# VARIABLES #
|
||||||
prune_out:
|
#############
|
||||||
for out in $$(find out -type f); do \
|
|
||||||
out_basename="$$(basename "$$out")"; \
|
|
||||||
out_ext="$${out_basename##*\.}"; \
|
|
||||||
rel_path="$$(echo "$${out}" | cut -c5-)"; \
|
|
||||||
if [ -f "public/$${rel_path}" ]; then \
|
|
||||||
continue 2; \
|
|
||||||
fi; \
|
|
||||||
base_rel_path="$${rel_path%%.$${out_ext}}"; \
|
|
||||||
src_path="src/$${base_rel_path}.upp$${out_ext}"; \
|
|
||||||
if [ -f "$$src_path" ]; then \
|
|
||||||
continue 2; \
|
|
||||||
fi; \
|
|
||||||
for layout in $$(find share/layouts -name "*.layout.upp$${out_ext}"); do \
|
|
||||||
layout="$$(basename "$$layout")"; \
|
|
||||||
layout="$${layout::$${#layout}-15}"; \
|
|
||||||
rel_path="$$(echo "$${out}" | cut -c5-)"; \
|
|
||||||
base_rel_path="$${rel_path%%.$${out_ext}}"; \
|
|
||||||
src_path="src/$${base_rel_path}.$${layout}.upp$${out_ext}"; \
|
|
||||||
if [ -f "$$src_path" ]; then \
|
|
||||||
continue 2; \
|
|
||||||
fi; \
|
|
||||||
done; \
|
|
||||||
echo "No source file found to match $${out}: removing it"; \
|
|
||||||
rm "$$out"; \
|
|
||||||
done
|
|
||||||
|
|
||||||
.SILENT: prune_microlog
|
|
||||||
.PHONY: prune_microlog
|
|
||||||
prune_microlog:
|
|
||||||
for post in $$(find src/microlog -name *.microlog.upphtml); do \
|
|
||||||
post_basename="$$(basename "$$post")"; \
|
|
||||||
post_src="blogs/microlog/$${post_basename::$${#post_basename}-17}.gmi"; \
|
|
||||||
if ! [ -f "$$post_src" ]; then \
|
|
||||||
echo "Microlog post source file $${post_src} has been deleted: removing $${post}"; \
|
|
||||||
rm -f "$$post" "$${post}.args"; \
|
|
||||||
fi; \
|
|
||||||
done
|
|
||||||
|
|
||||||
.SILENT: prune_blog
|
|
||||||
.PHONY: prune_blog
|
|
||||||
prune_blog:
|
|
||||||
for post in $$(find src/blog -name *.blog.upphtml); do \
|
|
||||||
post_basename="$$(basename "$$post")"; \
|
|
||||||
post_src="blogs/blog/$${post_basename::$${#post_basename}-13}.gmi"; \
|
|
||||||
if ! [ -f "$$post_src" ]; then \
|
|
||||||
echo "Blog post source file $${post_src} has been deleted: removing $${post}"; \
|
|
||||||
rm -f "$$post" "$${post}.args"; \
|
|
||||||
fi; \
|
|
||||||
done
|
|
||||||
|
|
||||||
src_files := $(filter-out %.args,$(wildcard src/*.upp*))
|
src_files := $(filter-out %.args,$(wildcard src/*.upp*))
|
||||||
src_files += $(filter-out %.args,$(wildcard src/**/*.upp*))
|
src_files += $(filter-out %.args,$(wildcard src/**/*.upp*))
|
||||||
src_files_ext := $(suffix $(src_files))
|
src_files_ext := $(suffix $(src_files))
|
||||||
out_files_ext := $(src_files_ext:.upp%=%)
|
out_deps_ext := $(src_files_ext:.upp%=%)
|
||||||
|
|
||||||
|
component_files := $(shell find -L share/components -type f)
|
||||||
layout_files := $(wildcard share/layouts/*.layout.upp*)
|
layout_files := $(wildcard share/layouts/*.layout.upp*)
|
||||||
layouts := $(notdir $(basename $(basename $(layout_files))))
|
layouts := $(notdir $(basename $(basename $(layout_files))))
|
||||||
out_files := $(addprefix out/,$(src_files:src/%=%))
|
|
||||||
remove_layout_from_out_files = out_files := $$(subst $(1).upp,.,$$(out_files))
|
|
||||||
$(foreach layout,$(layouts),$(eval $(call remove_layout_from_out_files,.$(layout))))
|
|
||||||
$(eval $(call remove_layout_from_out_files,))
|
|
||||||
|
|
||||||
define out_rule_for_ext_and_layout =
|
out_deps := $(addprefix out/,$(src_files:src/%=%))
|
||||||
out/%.$(1): src/%$(2).upp$(1)
|
remove_layout_from_out_deps = out_deps := $$(subst $(1).upp,.,$$(out_deps))
|
||||||
|
$(foreach layout,$(layouts),$(eval $(call remove_layout_from_out_deps,.$(layout))))
|
||||||
|
$(eval $(call remove_layout_from_out_deps,))
|
||||||
|
|
||||||
|
blogs := $(subst blogs/,,$(wildcard blogs/*))
|
||||||
|
|
||||||
|
|
||||||
|
#############
|
||||||
|
# FUNCTIONS #
|
||||||
|
#############
|
||||||
|
|
||||||
|
blog_post_target = src/$(blog)/%$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml
|
||||||
|
blog_post_src = blogs/$(blog)/%.$(BLOG_EXT_$(blog)) $$(wildcard bin/*)
|
||||||
|
blog_src = $(patsubst \
|
||||||
|
blogs/$(blog)/%.$(BLOG_EXT_$(blog)),\
|
||||||
|
src/$(blog)/%$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml,\
|
||||||
|
$(shell find -L "blogs/$(blog)" -type f -and -name *.$(BLOG_EXT_$(blog))))
|
||||||
|
default_layout_file = $(if \
|
||||||
|
$(shell [ -f "share/layouts/default.layout.$(ext)" ] && echo 1),\
|
||||||
|
share/layouts/default.layout.$(ext))
|
||||||
|
layout_file = $(if $(layout),share/layouts/$(layout).layout.upp$(ext),$(default_layout_file))
|
||||||
|
|
||||||
|
define out_rule =
|
||||||
|
out/%.$(ext): src/%$(if $(layout),.$(layout)).upp$(ext) $(layout_file) $(component_files)
|
||||||
./bin/mkws https://joeac.net "$$$$(realpath $$<)"
|
./bin/mkws https://joeac.net "$$$$(realpath $$<)"
|
||||||
endef
|
endef
|
||||||
|
|
||||||
$(foreach ext,$(sort $(out_files_ext)),\
|
define blog_rss_feed_rule =
|
||||||
$(eval $(call out_rule_for_ext_and_layout,$(ext),)))
|
out/$(blog)/rss.xml: src/$(blog)/rss.uppxml $(blog_src) $(wildcard bin/*)
|
||||||
$(foreach layout,$(layouts),\
|
./bin/mkws https://joeac.net "$$(realpath src/$(blog)/rss.uppxml)"
|
||||||
$(foreach ext,$(sort $(out_files_ext)),\
|
endef
|
||||||
$(eval $(call out_rule_for_ext_and_layout,$(ext),.$(layout)))))
|
|
||||||
|
define blog_rule =
|
||||||
|
.PHONY: $(blog)
|
||||||
|
$(blog): $(blog_src)
|
||||||
|
endef
|
||||||
|
|
||||||
|
define blog_post_rule =
|
||||||
|
$(blog_post_target): $(blog_post_src) $(let ext,html,$(layout_file) $(component_files))
|
||||||
|
./bin/blog \
|
||||||
|
-i $(blog) \
|
||||||
|
$(if $(BLOG_LAYOUT_$(blog)),-l $(BLOG_LAYOUT_$(blog))) \
|
||||||
|
$(BLOG_FLAGS_$(blog)) \
|
||||||
|
-f $$*.$(BLOG_EXT_$(blog))
|
||||||
|
endef
|
||||||
|
|
||||||
|
define prune_blog_rule =
|
||||||
|
.PHONY: prune_$(blog)
|
||||||
|
prune_$(blog):
|
||||||
|
./bin/prune_blog $(blog) *$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml
|
||||||
|
endef
|
||||||
|
|
||||||
|
|
||||||
|
#########
|
||||||
|
# RULES #
|
||||||
|
#########
|
||||||
|
|
||||||
|
all: bin/pp $(blogs) public out
|
||||||
|
|
||||||
|
bin/pp:
|
||||||
|
cd pp && $(MAKE)
|
||||||
|
|
||||||
.PHONY: out
|
.PHONY: out
|
||||||
out: $(out_files)
|
out: $(out_deps)
|
||||||
|
|
||||||
.PHONY: microlog
|
$(foreach ext,$(sort $(out_deps_ext)),\
|
||||||
microlog: $(patsubst blogs/microlog/%.gmi,src/microlog/%.microlog.upphtml,$(wildcard blogs/microlog/*.gmi))
|
$(eval $(out_rule)))
|
||||||
|
$(foreach layout,$(layouts),\
|
||||||
|
$(foreach ext,$(sort $(out_deps_ext)),\
|
||||||
|
$(eval $(out_rule))))
|
||||||
|
|
||||||
src/microlog/%.microlog.upphtml: blogs/microlog/%.gmi bin/blog bin/gmi2upphtml
|
$(foreach blog,$(blogs),$(eval $(blog_rule)))
|
||||||
./bin/blog -i microlog -l microlog -t -p gmi2upphtml -f $*.gmi
|
|
||||||
|
|
||||||
.PHONY: blog
|
$(foreach blog,$(blogs),\
|
||||||
blog: $(patsubst blogs/blog/%.gmi,src/blog/%.blog.upphtml,$(wildcard blogs/blog/*.gmi))
|
$(foreach layout,$(layouts),\
|
||||||
|
$(eval $(call blog_post_rule,$(blog),$(layout)))))
|
||||||
|
$(foreach blog,$(blogs),\
|
||||||
|
$(eval $(call blog_post_rule,$(blog),)))
|
||||||
|
|
||||||
src/blog/%.blog.upphtml: blogs/blog/%.gmi bin/blog bin/gmi2upphtml bin/cut_longlog_meta
|
$(foreach blog,$(blogs),$(eval $(blog_rss_feed_rule)))
|
||||||
./bin/blog -i blog -l blog -p 'cut_longlog_meta | gmi2upphtml' -a extract_longlog_meta -f $*.gmi
|
|
||||||
|
|
||||||
.PHONY: public
|
.PHONY: public
|
||||||
public: $(subst public/,out/,$(shell find -L public/ -type f))
|
public: $(subst public/,out/,$(shell find -L public/ -type f))
|
||||||
@@ -99,6 +102,19 @@ public: $(subst public/,out/,$(shell find -L public/ -type f))
|
|||||||
out/%: public/%
|
out/%: public/%
|
||||||
mkdir -p $(dir $@) && cp $< $@
|
mkdir -p $(dir $@) && cp $< $@
|
||||||
|
|
||||||
|
.PHONY: prune
|
||||||
|
prune: $(addprefix prune_,$(blogs)) prune_out
|
||||||
|
|
||||||
|
.PHONY: prune_out
|
||||||
|
prune_out:
|
||||||
|
./bin/prune_out
|
||||||
|
|
||||||
|
$(foreach blog,$(blogs),$(eval $(prune_blog_rule)))
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rf out src/microlog src/blog
|
rm -rf \
|
||||||
|
out \
|
||||||
|
$(foreach blog,$(blogs), \
|
||||||
|
$(foreach ext,upphtml upphtml.args,\
|
||||||
|
src/$(blog)/*$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).$(ext)))
|
||||||
|
|||||||
+4
-5
@@ -37,7 +37,7 @@ USAGE
|
|||||||
extract_args_cmd="head -n 0 | echo \"POST_TITLE=\\\"\${POST_TITLE}\\\"\""
|
extract_args_cmd="head -n 0 | echo \"POST_TITLE=\\\"\${POST_TITLE}\\\"\""
|
||||||
file_pattern="*"
|
file_pattern="*"
|
||||||
log_name=""
|
log_name=""
|
||||||
layout="default"
|
layout_name="default"
|
||||||
postprocess_cmd="echo"
|
postprocess_cmd="echo"
|
||||||
use_filenames_for_titles=0
|
use_filenames_for_titles=0
|
||||||
|
|
||||||
@@ -99,11 +99,10 @@ do
|
|||||||
dest="${SRC}/${log_name}/${name}${layout_ext}.upphtml"
|
dest="${SRC}/${log_name}/${name}${layout_ext}.upphtml"
|
||||||
if [ $use_filenames_for_titles -eq 1 ]
|
if [ $use_filenames_for_titles -eq 1 ]
|
||||||
then
|
then
|
||||||
title="$name"
|
export POST_TITLE="$name"
|
||||||
cat "$entry" | eval "POST_TITLE=\"${title}\" ${postprocess_cmd}" > "$dest"
|
cat "$entry" | eval "${postprocess_cmd}" > "$dest"
|
||||||
else
|
else
|
||||||
title="$(grep --regexp "^# " --max-count 1 "$entry" | cut -c3-)"
|
export POST_TITLE="$(grep --regexp "^# " --max-count 1 "$entry" | cut -c3-)"
|
||||||
export POST_TITLE="$title"
|
|
||||||
cat "$entry" | grep --regexp "^# " --invert-match | eval "$postprocess_cmd" > "$dest"
|
cat "$entry" | grep --regexp "^# " --invert-match | eval "$postprocess_cmd" > "$dest"
|
||||||
fi
|
fi
|
||||||
cat "$entry" | eval "$extract_args_cmd" > "${dest}.args"
|
cat "$entry" | eval "$extract_args_cmd" > "${dest}.args"
|
||||||
|
|||||||
@@ -53,7 +53,14 @@ close_all_open_blocks()
|
|||||||
|
|
||||||
escape_upphtml()
|
escape_upphtml()
|
||||||
{
|
{
|
||||||
echo "$1" | sed "s/\`/\\\\\`/g" | sed "s/\"/\\\\\"/g" | sed "s/\\\$/\\\\\$/g"
|
echo "$1" \
|
||||||
|
| sed "s/&/\\&/g" \
|
||||||
|
| sed "s/</\\</g" \
|
||||||
|
| sed "s/>/\\>/g" \
|
||||||
|
| sed "s/\"/\\"/g" \
|
||||||
|
| sed "s/'/\\'/g" \
|
||||||
|
| sed "s/\`/\\`/g" \
|
||||||
|
| sed "s/\\\$/\\\\\$/g"
|
||||||
}
|
}
|
||||||
|
|
||||||
close_all_open_blocks_except()
|
close_all_open_blocks_except()
|
||||||
|
|||||||
BIN
Binary file not shown.
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/home/joeac/src/joeac.net/http/pp/pp
|
||||||
Executable
+43
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
|
||||||
|
BLOG="$1"
|
||||||
|
BLOG_POST_PATTERN="${2:-*}"
|
||||||
|
|
||||||
|
SRC="$(realpath src)"
|
||||||
|
BLOGS="$(realpath blogs)"
|
||||||
|
LAYOUTS="$(realpath share/layouts)"
|
||||||
|
BLOG_DIR_IN_SRC="${SRC}/${BLOG}"
|
||||||
|
BLOG_EXT=gmi
|
||||||
|
|
||||||
|
for post in $(find "$BLOG_DIR_IN_SRC" -type f -and -name "$BLOG_POST_PATTERN")
|
||||||
|
do
|
||||||
|
last_segment="${post##*\.}"
|
||||||
|
if [ "$last_segment" = "args" ]
|
||||||
|
then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
post_rel_path="$(echo "$post" | cut -c${#BLOG_DIR_IN_SRC}- | cut -c3-)"
|
||||||
|
post_basename="${post_rel_path::${#post_rel_path}-${#last_segment}-1}"
|
||||||
|
post_src="${BLOGS}/${BLOG}/${post_basename}.${BLOG_EXT}"
|
||||||
|
if [ -f "$post_src" ]
|
||||||
|
then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
penultimate_segment="${post::${#post}-${#last_segment}-1}"
|
||||||
|
penultimate_segment="${penultimate_segment##*\.}"
|
||||||
|
post_basename="${post_rel_path::${#post_rel_path}-${#last_segment}-${#penultimate_segment}-2}"
|
||||||
|
if [ -f "${LAYOUTS}/${penultimate_segment}.layout.${last_segment}" ]
|
||||||
|
then
|
||||||
|
post_basename="${post_rel_path::${#post_rel_path}-${#last_segment}-${#penultimate_segment}-2}"
|
||||||
|
post_src="${BLOGS}/${BLOG}/${post_basename}.${BLOG_EXT}"
|
||||||
|
if [ -f "$post_src" ]
|
||||||
|
then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "No source file found to match post ${post} in blog ${BLOG}: removing ${post}"
|
||||||
|
rm -f "$post" "${post}.args"
|
||||||
|
done
|
||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
|
||||||
|
for out in $(find out -type f)
|
||||||
|
do
|
||||||
|
out_basename="$(basename "$out")"
|
||||||
|
out_ext="${out_basename##*\.}"
|
||||||
|
rel_path="$(echo "${out}" | cut -c5-)"
|
||||||
|
|
||||||
|
if [ -f "public/${rel_path}" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
base_rel_path="${rel_path%%.${out_ext}}"
|
||||||
|
src_path="src/${base_rel_path}.upp${out_ext}"
|
||||||
|
if [ -f "$src_path" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
for layout in $(find share/layouts -name "*.layout.upp${out_ext}"); do
|
||||||
|
layout="$(basename "$layout")"
|
||||||
|
layout="${layout::${#layout}-15}"
|
||||||
|
rel_path="$(echo "${out}" | cut -c5-)"
|
||||||
|
base_rel_path="${rel_path%%.${out_ext}}"
|
||||||
|
src_path="src/${base_rel_path}.${layout}.upp${out_ext}"
|
||||||
|
if [ -f "$src_path" ]; then
|
||||||
|
continue 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "No source file found to match ${out}: removing it"
|
||||||
|
rm "$out"
|
||||||
|
done
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
BLOG_LAYOUT_blog := blog
|
||||||
|
BLOG_EXT_blog := gmi
|
||||||
|
BLOG_FLAGS_blog = -p 'cut_longlog_meta | gmi2upphtml' -a extract_longlog_meta
|
||||||
|
|
||||||
|
BLOG_LAYOUT_microlog := microlog
|
||||||
|
BLOG_EXT_microlog := gmi
|
||||||
|
BLOG_FLAGS_microlog := -t -p gmi2upphtml
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pp
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2021, Adrian Emil Grigore <adrian.emil.grigore@gmail.com>
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
CC = cc
|
||||||
|
LD = ${CC}
|
||||||
|
|
||||||
|
PREFIX = /usr/local
|
||||||
|
MANPREFIX = ${PREFIX}/share/man
|
||||||
|
|
||||||
|
CFLAGS = -Wall -Wextra -pedantic
|
||||||
|
|
||||||
|
pp: pp.o
|
||||||
|
pp.o: pp.c
|
||||||
|
|
||||||
|
.c.o:
|
||||||
|
$(CC) $(CPPFLAGS) $< $(CFLAGS) -c -o $@
|
||||||
|
.o:
|
||||||
|
$(LD) $< $(LDFLAGS) -o $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf pp *.o
|
||||||
|
|
||||||
|
install: pp pp.1
|
||||||
|
mkdir -p $(DESTDIR)$(PREFIX)/bin
|
||||||
|
mkdir -p $(DESTDIR)$(MANPREFIX)/man1
|
||||||
|
cp pp $(DESTDIR)$(PREFIX)/bin/pp
|
||||||
|
cp pp.1 $(DESTDIR)$(MANPREFIX)/man1/
|
||||||
|
chmod 755 $(DESTDIR)$(PREFIX)/bin/pp
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm $(DESTDIR)$(PREFIX)/bin/pp
|
||||||
|
rm $(DESTDIR)$(MANPREFIX)/man1/pp.1
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
# `pp(1)`
|
||||||
|
|
||||||
|
a preprocessor
|
||||||
|
|
||||||
|
`pp(1)` allows embedding
|
||||||
|
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
|
||||||
|
code in files of any type by nesting it inside the `#!\n` token, where
|
||||||
|
`\n` is a new line. That means that if you'd like a simple loop or an
|
||||||
|
`if` inside an HTML file for instance,
|
||||||
|
you could use `pp(1)`.
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
For the following code:
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<title>pp(1) example</title>
|
||||||
|
<ul>
|
||||||
|
#!
|
||||||
|
i=1
|
||||||
|
while test $i -le 10
|
||||||
|
do
|
||||||
|
if test $((i % 2)) -eq 0
|
||||||
|
then
|
||||||
|
#!
|
||||||
|
<li class=even>$i</li>
|
||||||
|
#!
|
||||||
|
else
|
||||||
|
#!
|
||||||
|
<li class=odd>$i</li>
|
||||||
|
#!
|
||||||
|
fi
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
#!
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`pp(1)` outputs:
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<title>pp(1) example</title>
|
||||||
|
<ul>
|
||||||
|
<li class=odd>1</li>
|
||||||
|
<li class=even>2</li>
|
||||||
|
<li class=odd>3</li>
|
||||||
|
<li class=even>4</li>
|
||||||
|
<li class=odd>5</li>
|
||||||
|
<li class=even>6</li>
|
||||||
|
<li class=odd>7</li>
|
||||||
|
<li class=even>8</li>
|
||||||
|
<li class=odd>9</li>
|
||||||
|
<li class=even>10</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
You can also pass arguments to it by appending them to the command call:
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<title>$1</title>
|
||||||
|
<p>
|
||||||
|
#!
|
||||||
|
echo $2
|
||||||
|
#!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Calling the above code with
|
||||||
|
`pp tmp.upphtml 'pp(1) example' 'hello, world'`
|
||||||
|
will result in:
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<title>pp(1) example</title>
|
||||||
|
<p>
|
||||||
|
hello world
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Pipes
|
||||||
|
|
||||||
|
`pp(1)`'s `stdin` is sent to the child
|
||||||
|
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<title>pp(1) example</title>
|
||||||
|
<p>
|
||||||
|
#!
|
||||||
|
cat
|
||||||
|
#!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
`echo 'hello, world' | pp tmp.upphtml` will output:
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<title>pp(1) example</title>
|
||||||
|
<p>
|
||||||
|
hello, world
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
`pp(1)` also takes an optional `-d` flag. If passed, `pp(1)` will dump
|
||||||
|
the generated
|
||||||
|
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
|
||||||
|
code instead of executing it:
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<title>pp(1) example</title>
|
||||||
|
<ul>
|
||||||
|
#!
|
||||||
|
i=1
|
||||||
|
while test $i -le 10
|
||||||
|
do
|
||||||
|
if test $((i % 2)) -eq 0
|
||||||
|
then
|
||||||
|
#!
|
||||||
|
<li class=even>$i</li>
|
||||||
|
#!
|
||||||
|
else
|
||||||
|
#!
|
||||||
|
<li class=odd>$i</li>
|
||||||
|
#!
|
||||||
|
fi
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
#!
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`pp -d tmp.upphtml` will output:
|
||||||
|
|
||||||
|
echo "<!doctype html>
|
||||||
|
<title>pp(1) example</title>
|
||||||
|
<ul>"
|
||||||
|
i=1
|
||||||
|
while test $i -le 10
|
||||||
|
do
|
||||||
|
if test $((i % 2)) -eq 0
|
||||||
|
then
|
||||||
|
echo " <li class=even>$i</li>"
|
||||||
|
else
|
||||||
|
echo " <li class=odd>$i</li>"
|
||||||
|
fi
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
echo "</ul>"
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang=en>
|
||||||
|
<title>pp(1)</title>
|
||||||
|
<meta charset=UTF-8>
|
||||||
|
<meta name=viewport content='width=device-width'>
|
||||||
|
<link rel=stylesheet href=../theme.css>
|
||||||
|
<body>
|
||||||
|
<a href=../#myprojects>Home</a>
|
||||||
|
#!
|
||||||
|
addcls() {
|
||||||
|
sed "s/<code>/<code class=language-bash>/g"
|
||||||
|
}
|
||||||
|
smu README | addcls
|
||||||
|
#!
|
||||||
|
<h2>Files</h2>
|
||||||
|
<ul>
|
||||||
|
#!
|
||||||
|
for f in *
|
||||||
|
do
|
||||||
|
case $f in
|
||||||
|
*.upp*|*.html|*.tgz)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
#!
|
||||||
|
<li><a href=$f>$f</a></li>
|
||||||
|
#!
|
||||||
|
done
|
||||||
|
#!
|
||||||
|
</ul>
|
||||||
|
<h2>Download</h2>
|
||||||
|
<a href=pp.tgz>pp.tgz</a>
|
||||||
|
<script src=../prism.js data-manual></script>
|
||||||
|
<script src=../enhance.js></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
* <https://github.com/radare/spp>
|
||||||
|
* <https://werc.cat-v.org/docs/rc-template-lang>
|
||||||
|
* <https://code.9front.org/hg/werc/file/92f7463dac1a/bin/template.awk>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
.Dd Apr 25, 2019
|
||||||
|
.Dt pp 1
|
||||||
|
.Os
|
||||||
|
.Sh NAME
|
||||||
|
.Nm pp
|
||||||
|
.Nd preprocessor
|
||||||
|
.Sh SYNOPSIS
|
||||||
|
.Nm pp
|
||||||
|
.Op Fl d
|
||||||
|
.Ar file
|
||||||
|
.Op args ...
|
||||||
|
.Sh DESCRIPTION
|
||||||
|
The
|
||||||
|
.Nm
|
||||||
|
utility processes
|
||||||
|
.Ar file
|
||||||
|
to produce output that is used as input to another
|
||||||
|
program.
|
||||||
|
.Pp
|
||||||
|
It executes code enclosed in
|
||||||
|
.Dq #!\en
|
||||||
|
delimiters using
|
||||||
|
.Xr sh 1 .
|
||||||
|
.Op args ...
|
||||||
|
are passed to the embbeded script and are available
|
||||||
|
via the usual
|
||||||
|
.Em $1, $2, ...
|
||||||
|
positional parameters.
|
||||||
|
.Pp
|
||||||
|
The options are as follows:
|
||||||
|
.Bl -tag -width Ds
|
||||||
|
.It Fl d
|
||||||
|
.Nm
|
||||||
|
will dump the generated
|
||||||
|
.Xr sh 1
|
||||||
|
code instead of executing it. This is useful for debugging.
|
||||||
|
.El
|
||||||
|
.Sh EXIT STATUS
|
||||||
|
.Ex -std
|
||||||
|
.Sh EXAMPLES
|
||||||
|
Preprocess file with arguments:
|
||||||
|
.Pp
|
||||||
|
.Dl $ pp file.upphtml a b
|
||||||
|
.Pp
|
||||||
|
.Sh SEE ALSO
|
||||||
|
.Xr sh 1
|
||||||
+369
@@ -0,0 +1,369 @@
|
|||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#if !__APPLE__
|
||||||
|
/*
|
||||||
|
* Appends src to string dst of size dsize (unlike strncat, dsize is the
|
||||||
|
* full size of dst, not space left). At most dsize-1 characters
|
||||||
|
* will be copied. Always NUL terminates (unless dsize <= strlen(dst)).
|
||||||
|
* Returns strlen(src) + MIN(dsize, strlen(initial dst)).
|
||||||
|
* If retval >= dsize, truncation occurred.
|
||||||
|
*/
|
||||||
|
size_t
|
||||||
|
strlcat(char *dst, const char *src, size_t dsize)
|
||||||
|
{
|
||||||
|
const char *odst = dst;
|
||||||
|
const char *osrc = src;
|
||||||
|
size_t n = dsize;
|
||||||
|
size_t dlen;
|
||||||
|
|
||||||
|
/* Find the end of dst and adjust bytes left but don't go past end. */
|
||||||
|
while (n-- != 0 && *dst != '\0')
|
||||||
|
dst++;
|
||||||
|
dlen = dst - odst;
|
||||||
|
n = dsize - dlen;
|
||||||
|
|
||||||
|
if (n-- == 0)
|
||||||
|
return(dlen + strlen(src));
|
||||||
|
while (*src != '\0') {
|
||||||
|
if (n != 0) {
|
||||||
|
*dst++ = *src;
|
||||||
|
n--;
|
||||||
|
}
|
||||||
|
src++;
|
||||||
|
}
|
||||||
|
*dst = '\0';
|
||||||
|
|
||||||
|
return(dlen + (src - osrc)); /* count does not include NUL */
|
||||||
|
}
|
||||||
|
#endif /* !__APPLE__ */
|
||||||
|
|
||||||
|
#define MAXSIZE 64000
|
||||||
|
#define MAXINDENTSIZE 80
|
||||||
|
#define MAXARGSIZE 20
|
||||||
|
|
||||||
|
char *argv0;
|
||||||
|
|
||||||
|
int
|
||||||
|
efgetc(FILE *fp)
|
||||||
|
{
|
||||||
|
int c;
|
||||||
|
|
||||||
|
c = fgetc(fp);
|
||||||
|
|
||||||
|
if (c == EOF &&
|
||||||
|
ferror(fp) != 0) {
|
||||||
|
perror(argv0);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
efseek(FILE *fp, long offset, int whence)
|
||||||
|
{
|
||||||
|
int s;
|
||||||
|
|
||||||
|
s = fseek(fp, offset, whence);
|
||||||
|
|
||||||
|
if (s == -1) {
|
||||||
|
perror(argv0);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
eungetc(int c, FILE *fp)
|
||||||
|
{
|
||||||
|
int uc;
|
||||||
|
|
||||||
|
uc = ungetc(c, fp);
|
||||||
|
|
||||||
|
if (uc == EOF) {
|
||||||
|
perror(argv0);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uc;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t
|
||||||
|
estrlcat(char *dst, const char *src, size_t dstsize)
|
||||||
|
{
|
||||||
|
size_t n;
|
||||||
|
|
||||||
|
n = strlcat(dst, src, dstsize);
|
||||||
|
|
||||||
|
if (n >= dstsize) {
|
||||||
|
fprintf(stderr, "%s: Buffer overflow\n", argv0);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
eclose(int d)
|
||||||
|
{
|
||||||
|
int s;
|
||||||
|
|
||||||
|
s = close(d);
|
||||||
|
|
||||||
|
if (s == -1) {
|
||||||
|
perror(argv0);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *
|
||||||
|
strappend(char *s, char c)
|
||||||
|
{
|
||||||
|
size_t len;
|
||||||
|
|
||||||
|
len = strlen(s);
|
||||||
|
|
||||||
|
s[len] = c;
|
||||||
|
s[len + 1] = '\0';
|
||||||
|
|
||||||
|
return s ;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
shebang(FILE *fp)
|
||||||
|
{
|
||||||
|
int c;
|
||||||
|
|
||||||
|
c = efgetc(fp);
|
||||||
|
|
||||||
|
if (c != '#') {
|
||||||
|
eungetc(c, fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
c = efgetc(fp);
|
||||||
|
|
||||||
|
if (c != '!') {
|
||||||
|
eungetc(c, fp);
|
||||||
|
eungetc('#', fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
c = efgetc(fp);
|
||||||
|
|
||||||
|
if (c == '\n') {
|
||||||
|
eungetc(c, fp);
|
||||||
|
eungetc('!', fp);
|
||||||
|
eungetc('#', fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
c = efgetc(fp);
|
||||||
|
if (c == '\n')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
token(FILE *fp, int consume)
|
||||||
|
{
|
||||||
|
int c;
|
||||||
|
|
||||||
|
c = efgetc(fp);
|
||||||
|
|
||||||
|
if (c != '#') {
|
||||||
|
eungetc(c, fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
c = efgetc(fp);
|
||||||
|
if (c != '!') {
|
||||||
|
eungetc(c, fp);
|
||||||
|
eungetc('#', fp);
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
c = efgetc(fp);
|
||||||
|
|
||||||
|
if (c != '\n') {
|
||||||
|
eungetc(c, fp);
|
||||||
|
eungetc('!', fp);
|
||||||
|
eungetc('#', fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consume == 0) {
|
||||||
|
eungetc(c, fp);
|
||||||
|
eungetc('!', fp);
|
||||||
|
eungetc('#', fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t
|
||||||
|
output(char *s)
|
||||||
|
{
|
||||||
|
return estrlcat(s, "echo \"", MAXSIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t
|
||||||
|
outputend(char *s)
|
||||||
|
{
|
||||||
|
return estrlcat(s, "\"\n", MAXSIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
char s[MAXSIZE] = "", indent[MAXINDENTSIZE] = "",
|
||||||
|
*eargv[MAXARGSIZE] = {"sh", "-c", s, "sh", NULL};
|
||||||
|
FILE *fp;
|
||||||
|
int size, c, i, fd[2], o = 0, status = 0, d = 0;
|
||||||
|
|
||||||
|
|
||||||
|
argv0 = argv[0];
|
||||||
|
if (argc == 1) {
|
||||||
|
fprintf(stderr, "usage: %s file [args ...]\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(argv[1], "-d") == 0) {
|
||||||
|
if (argc == 2) {
|
||||||
|
fprintf(stderr, "usage: %s file [args ...]\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
d = 1;
|
||||||
|
fp = fopen(argv[2], "r");
|
||||||
|
} else {
|
||||||
|
fp = fopen(argv[1], "r");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!fp) {
|
||||||
|
fprintf(stderr, "%s: %s: %s\n",
|
||||||
|
argv[0], d ? argv[2] : argv[1], strerror(errno));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
efseek (fp, 0, SEEK_END);
|
||||||
|
size = ftell(fp);
|
||||||
|
|
||||||
|
if(size == 0) return 0;
|
||||||
|
|
||||||
|
efseek(fp, -1, SEEK_END);
|
||||||
|
c = efgetc(fp);
|
||||||
|
|
||||||
|
if (c != '\n') {
|
||||||
|
fprintf(stderr, "%s: %s: No newline at end of file\n",
|
||||||
|
argv[0], d ? argv[2] : argv[1]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
efseek(fp, 0, SEEK_SET);
|
||||||
|
|
||||||
|
shebang(fp);
|
||||||
|
|
||||||
|
if (!token(fp, 1)) {
|
||||||
|
output(s);
|
||||||
|
o = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(;;) {
|
||||||
|
if (token(fp, 1)) {
|
||||||
|
if (!o) {
|
||||||
|
c = efgetc(fp);
|
||||||
|
if (c == EOF)
|
||||||
|
goto done;
|
||||||
|
eungetc(c, fp);
|
||||||
|
output(s);
|
||||||
|
} else
|
||||||
|
outputend(s);
|
||||||
|
o = !o;
|
||||||
|
}
|
||||||
|
|
||||||
|
c = efgetc(fp);
|
||||||
|
if (c == EOF)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (c == '\n') {
|
||||||
|
indent[0] = 0;
|
||||||
|
c = efgetc(fp);
|
||||||
|
if (c == EOF)
|
||||||
|
break;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
if (c != ' ' && c != '\t')
|
||||||
|
break;
|
||||||
|
strappend(indent, c);
|
||||||
|
c = efgetc(fp);
|
||||||
|
if (c == EOF)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
eungetc(c, fp);
|
||||||
|
|
||||||
|
if (token(fp, 0) && o)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
strappend(s, '\n');
|
||||||
|
estrlcat(s, indent, MAXSIZE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
strappend(s, c);
|
||||||
|
}
|
||||||
|
if (o) {
|
||||||
|
outputend(s);
|
||||||
|
o = 0;
|
||||||
|
}
|
||||||
|
done:
|
||||||
|
|
||||||
|
if(d) {
|
||||||
|
puts(s);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pipe(fd) == -1) {
|
||||||
|
perror(argv0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (fork()) {
|
||||||
|
case -1:
|
||||||
|
perror(argv0);
|
||||||
|
return 1;
|
||||||
|
case 0:
|
||||||
|
eclose(fd[1]);
|
||||||
|
eclose(fd[0]);
|
||||||
|
for (i = 2; i < argc; i++)
|
||||||
|
eargv[i+2] = argv[i];
|
||||||
|
|
||||||
|
if (execv("/bin/sh", eargv) == -1) {
|
||||||
|
perror(argv0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
eclose(fd[1]);
|
||||||
|
eclose(fd[0]);
|
||||||
|
if (wait(&status) == -1) {
|
||||||
|
perror(argv0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -1,4 +1,11 @@
|
|||||||
<!-- Primary Meta Tags -->
|
#!
|
||||||
|
if [ -z "${NO_META}" ]; then
|
||||||
|
#!
|
||||||
|
|
||||||
<title>${TITLE}</title>
|
<title>${TITLE}</title>
|
||||||
<meta name="title" content="${TITLE}" />
|
<meta name='title' content='${TITLE}' />
|
||||||
<meta name="description" content="${DESCRIPTION}" />
|
<meta name='description' content='${DESCRIPTION}' />
|
||||||
|
|
||||||
|
#!
|
||||||
|
fi
|
||||||
|
#!
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ CANONICAL_URL="${SITE_URL}/blog/$(basename "${PAGE}")"
|
|||||||
#!
|
#!
|
||||||
pp "${SHARE}"/components/head.upphtml
|
pp "${SHARE}"/components/head.upphtml
|
||||||
#!
|
#!
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/css/post.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -23,7 +25,7 @@ pp "${SHARE}"/components/navbar.upphtml
|
|||||||
<article class='h-entry'>
|
<article class='h-entry'>
|
||||||
|
|
||||||
<aside>
|
<aside>
|
||||||
<span>This is a blog post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
|
<span>This is a <a href='/blog'>blog</a> post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
|
||||||
#!
|
#!
|
||||||
if [ -z "${DATE_UPDATED}" ]; then
|
if [ -z "${DATE_UPDATED}" ]; then
|
||||||
#!
|
#!
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ pp "${SHARE}"/components/head.upphtml
|
|||||||
<body>
|
<body>
|
||||||
#!
|
#!
|
||||||
pp "${SHARE}"/components/navbar.upphtml
|
pp "${SHARE}"/components/navbar.upphtml
|
||||||
|
#!
|
||||||
|
<main>
|
||||||
|
#!
|
||||||
pp "$1" "$2"
|
pp "$1" "$2"
|
||||||
#!
|
#!
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ CANONICAL_URL="${SITE_URL}/microlog/$(basename "${SRC}")"
|
|||||||
#!
|
#!
|
||||||
pp "${SHARE}"/components/head.upphtml
|
pp "${SHARE}"/components/head.upphtml
|
||||||
#!
|
#!
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/css/post.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -23,7 +25,7 @@ pp "${SHARE}"/components/navbar.upphtml
|
|||||||
<article class="h-entry">
|
<article class="h-entry">
|
||||||
|
|
||||||
<aside>
|
<aside>
|
||||||
<span>This is a microlog post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
|
<span>This is a <a href='/microlog'>microlog</a> post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
|
||||||
<span>Published:
|
<span>Published:
|
||||||
#!
|
#!
|
||||||
DATE="${DATE_PUBLISHED}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
|
DATE="${DATE_PUBLISHED}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!
|
||||||
|
pp $@
|
||||||
|
#!
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
.Dd Apr 25, 2019
|
|
||||||
.Dt pp 1
|
|
||||||
.Os
|
|
||||||
.Sh NAME
|
|
||||||
.Nm pp
|
|
||||||
.Nd preprocessor
|
|
||||||
.Sh SYNOPSIS
|
|
||||||
.Nm pp
|
|
||||||
.Op Fl d
|
|
||||||
.Ar file
|
|
||||||
.Op args ...
|
|
||||||
.Sh DESCRIPTION
|
|
||||||
The
|
|
||||||
.Nm
|
|
||||||
utility processes
|
|
||||||
.Ar file
|
|
||||||
to produce output that is used as input to another
|
|
||||||
program.
|
|
||||||
.Pp
|
|
||||||
It executes code enclosed in
|
|
||||||
.Dq #!\en
|
|
||||||
delimiters using
|
|
||||||
.Xr sh 1 .
|
|
||||||
.Op args ...
|
|
||||||
are passed to the embbeded script and are available
|
|
||||||
via the usual
|
|
||||||
.Em $1, $2, ...
|
|
||||||
positional parameters.
|
|
||||||
.Pp
|
|
||||||
The options are as follows:
|
|
||||||
.Bl -tag -width Ds
|
|
||||||
.It Fl d
|
|
||||||
.Nm
|
|
||||||
will dump the generated
|
|
||||||
.Xr sh 1
|
|
||||||
code instead of executing it. This is useful for debugging.
|
|
||||||
.El
|
|
||||||
.Sh EXIT STATUS
|
|
||||||
.Ex -std
|
|
||||||
.Sh EXAMPLES
|
|
||||||
Preprocess file with arguments:
|
|
||||||
.Pp
|
|
||||||
.Dl $ pp file.upphtml a b
|
|
||||||
.Pp
|
|
||||||
.Sh SEE ALSO
|
|
||||||
.Xr sh 1
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/home/joeac/src/joeac.net/http/pp/pp.1
|
||||||
@@ -4,7 +4,7 @@ SITE_URL="$2"
|
|||||||
export TITLE="Joe Carstairs"
|
export TITLE="Joe Carstairs"
|
||||||
export DESCRIPTION="Joe Carstairs' personal website"
|
export DESCRIPTION="Joe Carstairs' personal website"
|
||||||
|
|
||||||
POSTS="$(ls "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\.blog.upphtml$")"
|
POSTS="$(ls --reverse "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\.blog.upphtml$")"
|
||||||
set --
|
set --
|
||||||
for post in $POSTS
|
for post in $POSTS
|
||||||
do
|
do
|
||||||
@@ -28,8 +28,6 @@ YEARS="$@"
|
|||||||
pp "${SHARE}"/components/meta.upphtml
|
pp "${SHARE}"/components/meta.upphtml
|
||||||
#!
|
#!
|
||||||
|
|
||||||
<main>
|
|
||||||
|
|
||||||
<section class='h-feed'>
|
<section class='h-feed'>
|
||||||
|
|
||||||
<h1>My blog</h1>
|
<h1>My blog</h1>
|
||||||
@@ -40,7 +38,7 @@ pp "${SHARE}"/components/meta.upphtml
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p><a href='/blog/subscribe'>How to subscribe to this blog</a></p>
|
<p><a href='/subscribe'>How to subscribe to my logs</a></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
#!
|
#!
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#!
|
||||||
|
SITE_URL="$1"
|
||||||
|
POSTS="$(ls --reverse "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.blog.upphtml$")"
|
||||||
|
NOW_RFC_822="$(TZ=UT date +"%a, %d %b %Y %H:%M:%S UT")"
|
||||||
|
#!
|
||||||
|
|
||||||
|
<rss version='2.0'>
|
||||||
|
|
||||||
|
<channel>
|
||||||
|
<title>Joe Carstairs’ blog</title>
|
||||||
|
<description>Short posts on random topics I find interesting</description>
|
||||||
|
<link>${SITE_URL}/blog</link>
|
||||||
|
<image>${SITE_URL}/images/headshot.webp</image>
|
||||||
|
<language>en-GB</language>
|
||||||
|
<pubDate>${NOW_RFC_822}</pubDate>
|
||||||
|
|
||||||
|
#!
|
||||||
|
for post in $POSTS; do
|
||||||
|
POST_ID="${post::${#post}-13}"
|
||||||
|
source "${SRC}/blog/${post}.args"
|
||||||
|
DATE_PUBLISHED_RFC_822="$(TZ=UT date --date="${POST_ID::10}" +"%a, %d %b %Y %H:%M:%S UT")"
|
||||||
|
#!
|
||||||
|
<item>
|
||||||
|
|
||||||
|
<title>${POST_TITLE}</title>
|
||||||
|
<link>${SITE_URL}/blog/${POST_ID}</link>
|
||||||
|
<guid isPermaLink='true'>${SITE_URL}/blog/${POST_ID}</guid>
|
||||||
|
<pubDate>${DATE_PUBLISHED_RFC_822}</pubDate>
|
||||||
|
<description>
|
||||||
|
#!
|
||||||
|
NO_META=1 pp "${SRC}/blog/$post" "${SITE_URL}" \
|
||||||
|
| sed "s/&/\\&/g" \
|
||||||
|
| sed "s/</\\</g" \
|
||||||
|
| sed "s/>/\\>/g" \
|
||||||
|
| sed "s/\"/\\"/g" \
|
||||||
|
| sed "s/'/\\'/g" \
|
||||||
|
| sed "s/\`/\\`/g" \
|
||||||
|
| sed "s/\\\$/\\\\\$/g"
|
||||||
|
#!
|
||||||
|
</description>
|
||||||
|
<author>Joe Carstairs</author>
|
||||||
|
|
||||||
|
</item>
|
||||||
|
#!
|
||||||
|
done
|
||||||
|
#!
|
||||||
|
|
||||||
|
</channel>
|
||||||
|
|
||||||
|
</rss>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!
|
||||||
|
SITE_URL="${1}"
|
||||||
|
#!
|
||||||
|
<meta http-equiv="refresh" content="0" ; url="${SITE_URL}/subscribe" />
|
||||||
@@ -8,7 +8,6 @@ pp "${SHARE}"/components/meta.upphtml
|
|||||||
pp "${SHARE}"/components/otp-dialog.upphtml
|
pp "${SHARE}"/components/otp-dialog.upphtml
|
||||||
#!
|
#!
|
||||||
|
|
||||||
<main>
|
|
||||||
<form class=contact-form>
|
<form class=contact-form>
|
||||||
<h1>Contact me</h1>
|
<h1>Contact me</h1>
|
||||||
<p hidden class=error></p>
|
<p hidden class=error></p>
|
||||||
@@ -25,7 +24,7 @@ pp "${SHARE}"/components/otp-dialog.upphtml
|
|||||||
<h1>You sent me a message!</h1>
|
<h1>You sent me a message!</h1>
|
||||||
<p>Thanks for that. I may be in touch.</p>
|
<p>Thanks for that. I may be in touch.</p>
|
||||||
<p>In case you forgot, your message was this:</p>
|
<p>In case you forgot, your message was this:</p>
|
||||||
<hr>
|
<hr />
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Name</dt> <dd class=sentname>???</dd>
|
<dt>Name</dt> <dd class=sentname>???</dd>
|
||||||
<dt>Email</dt> <dd class=sentemail>???</dd>
|
<dt>Email</dt> <dd class=sentemail>???</dd>
|
||||||
@@ -95,4 +94,3 @@ pp "${SHARE}"/components/otp-dialog.upphtml
|
|||||||
await submitOtpForm(selectors);
|
await submitOtpForm(selectors);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</main>
|
|
||||||
|
|||||||
@@ -305,11 +305,6 @@ strong {
|
|||||||
|
|
||||||
/** Blog feed */
|
/** Blog feed */
|
||||||
|
|
||||||
.h-feed ul {
|
|
||||||
list-style: none;
|
|
||||||
margin-inline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Block quotes */
|
/** Block quotes */
|
||||||
blockquote {
|
blockquote {
|
||||||
padding-inline-start: var(--spacing-inline-lg);
|
padding-inline-start: var(--spacing-inline-lg);
|
||||||
@@ -317,6 +312,10 @@ blockquote {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blockquote :first-child {
|
||||||
|
margin-block-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
blockquote footer {
|
blockquote footer {
|
||||||
font-style: initial;
|
font-style: initial;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
margin-block-start: var(--spacing-block-md);
|
margin-block-start: var(--spacing-block-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-feed ul:has(> .h-entry) {
|
||||||
|
list-style-type: none;
|
||||||
|
margin-inline: 0;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.h-feed .h-entry {
|
.h-feed .h-entry {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export DESCRIPTION="Error page for Joe Carstairs' personal website"
|
|||||||
pp "${SHARE}"/components/meta.upphtml
|
pp "${SHARE}"/components/meta.upphtml
|
||||||
#!
|
#!
|
||||||
|
|
||||||
<main>
|
|
||||||
<section>
|
<section>
|
||||||
<h1>Got lost?</h1>
|
<h1>Got lost?</h1>
|
||||||
|
|
||||||
@@ -13,4 +12,3 @@ pp "${SHARE}"/components/meta.upphtml
|
|||||||
my <a href='/'>homepage</a> instead.
|
my <a href='/'>homepage</a> instead.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export DESCRIPTION="Joe Carstairs' personal website"
|
|||||||
pp "${SHARE}"/components/meta.upphtml
|
pp "${SHARE}"/components/meta.upphtml
|
||||||
#!
|
#!
|
||||||
|
|
||||||
<main>
|
|
||||||
<section class="h-card">
|
<section class="h-card">
|
||||||
<div>
|
<div>
|
||||||
<img class="u-photo" src="/images/headshot.webp" height="96" width="96" />
|
<img class="u-photo" src="/images/headshot.webp" height="96" width="96" />
|
||||||
@@ -46,4 +45,3 @@ pp "${SHARE}"/components/meta.upphtml
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!
|
||||||
|
SITE_URL="${1}"
|
||||||
|
#!
|
||||||
|
<meta http-equiv="refresh" content="0" ; url="${SITE_URL}/microlog/rss.xml" />
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!
|
||||||
|
PAGE="$1"
|
||||||
|
SITE_URL="$2"
|
||||||
|
export TITLE="Joe Carstairs"
|
||||||
|
export DESCRIPTION="Joe Carstairs' personal website"
|
||||||
|
|
||||||
|
POSTS="$(ls --reverse "${SRC}/microlog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.microlog.upphtml$")"
|
||||||
|
set --
|
||||||
|
for post in $POSTS
|
||||||
|
do
|
||||||
|
this_year="$( echo "$post" | cut -c1-4 )"
|
||||||
|
has_this_year=0
|
||||||
|
for year in $@
|
||||||
|
do
|
||||||
|
if [ "$this_year" = "$year" ]
|
||||||
|
then
|
||||||
|
has_this_year=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ $has_this_year -eq 0 ]
|
||||||
|
then
|
||||||
|
set -- $@ "$this_year"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
YEARS="$@"
|
||||||
|
|
||||||
|
pp "${SHARE}"/components/meta.upphtml
|
||||||
|
#!
|
||||||
|
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<section class='h-feed'>
|
||||||
|
|
||||||
|
<h1>My microlog</h1>
|
||||||
|
|
||||||
|
<aside>
|
||||||
|
<p>This microlog is written by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</p>
|
||||||
|
<p hidden><a class='u-url' href='${SITE_URL}/microlog'>Permalink</a></p>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p><a href='/subscribe'>How to subscribe to my logs</a></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
#!
|
||||||
|
for year in $YEARS; do
|
||||||
|
#!
|
||||||
|
<h2>${year}</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
#!
|
||||||
|
for post in $POSTS; do if [ "$(echo "${post}" | cut -c1-4)" = "$year" ]; then
|
||||||
|
title="$(grep "^export TITLE=" "${SRC}/microlog/${post}" | cut -c15-)"
|
||||||
|
title="${title::${#title}-16}"
|
||||||
|
date_published="$(echo "${post}" | cut -c1-10)"
|
||||||
|
href="${SITE_URL}/microlog/${post::${#post}-17}.html"
|
||||||
|
#!
|
||||||
|
|
||||||
|
<li class='h-entry'>
|
||||||
|
<h3><a class='u-url p-name' href='${href}'>${title}</a></h3>
|
||||||
|
<section class='e-content'>
|
||||||
|
#!
|
||||||
|
pp "${SRC}/microlog/${post}" "${SITE_URL}"
|
||||||
|
#!
|
||||||
|
</section>
|
||||||
|
#!
|
||||||
|
DATE="${date_published}" CLASS="dt-published" ATTRIBUTES="hidden" \
|
||||||
|
pp "${SHARE}/components/formatted_date.upphtml"
|
||||||
|
#!
|
||||||
|
</li>
|
||||||
|
|
||||||
|
#!
|
||||||
|
fi; done
|
||||||
|
#!
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
#!
|
||||||
|
done
|
||||||
|
#!
|
||||||
|
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#!
|
||||||
|
SITE_URL="$1"
|
||||||
|
POSTS="$(ls --reverse "${SRC}/microlog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.microlog.upphtml$")"
|
||||||
|
NOW_RFC_822="$(TZ=UT date +"%a, %d %b %Y %H:%M:%S UT")"
|
||||||
|
#!
|
||||||
|
|
||||||
|
<rss version='2.0'>
|
||||||
|
|
||||||
|
<channel>
|
||||||
|
<title>Joe Carstairs’ microlog</title>
|
||||||
|
<description>My private X feed I guess?</description>
|
||||||
|
<link>${SITE_URL}/microlog</link>
|
||||||
|
<image>${SITE_URL}/images/headshot.webp</image>
|
||||||
|
<language>en-GB</language>
|
||||||
|
<pubDate>${NOW_RFC_822}</pubDate>
|
||||||
|
|
||||||
|
#!
|
||||||
|
for post in $POSTS; do
|
||||||
|
POST_ID="${post::${#post}-17}"
|
||||||
|
source "${SRC}/microlog/${post}.args"
|
||||||
|
DATE_PUBLISHED_RFC_822="$(TZ=UT date --date="${POST_ID::10}" +"%a, %d %b %Y %H:%M:%S UT")"
|
||||||
|
#!
|
||||||
|
<item>
|
||||||
|
|
||||||
|
<title>${POST_TITLE}</title>
|
||||||
|
<link>${SITE_URL}/microlog/${POST_ID}</link>
|
||||||
|
<guid isPermaLink='true'>${SITE_URL}/microlog/${POST_ID}</guid>
|
||||||
|
<pubDate>${DATE_PUBLISHED_RFC_822}</pubDate>
|
||||||
|
<description>
|
||||||
|
#!
|
||||||
|
NO_META=1 pp "${SRC}/microlog/$post" "${SITE_URL}" \
|
||||||
|
| sed "s/&/\\&/g" \
|
||||||
|
| sed "s/</\\</g" \
|
||||||
|
| sed "s/>/\\>/g" \
|
||||||
|
| sed "s/\"/\\"/g" \
|
||||||
|
| sed "s/'/\\'/g" \
|
||||||
|
| sed "s/\`/\\`/g" \
|
||||||
|
| sed "s/\\\$/\\\\\$/g"
|
||||||
|
#!
|
||||||
|
</description>
|
||||||
|
<author>Joe Carstairs</author>
|
||||||
|
|
||||||
|
</item>
|
||||||
|
#!
|
||||||
|
done
|
||||||
|
#!
|
||||||
|
|
||||||
|
</channel>
|
||||||
|
|
||||||
|
</rss>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#!
|
||||||
|
PAGE="$1"
|
||||||
|
SITE_URL="$2"
|
||||||
|
export TITLE="How to subscribe to joeac's blog"
|
||||||
|
export DESCRIPTION="How to subscribe to joeac's blog"
|
||||||
|
#!
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h1>How to subscribe to my logs</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The short version: use my <a href='/blog/rss.xml'>blog RSS feed</a> and
|
||||||
|
<a href='/microlog/rss.xml'>microlog RSS feed</a>, or my
|
||||||
|
<a href='gemini://joeac.net/logs/loglog.gmi'>gemlog</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If neither of those things means anything to you, no worries. You could just
|
||||||
|
look at my website every once in a while, but that gets old. A better way is to
|
||||||
|
use my RSS file. Here's how.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
An RSS file, if you don't know, is a file containing a list of posts in a format
|
||||||
|
that's so stupid even computers can understand it. Mine is at
|
||||||
|
<a href='https://joeac.net/rss.xml'>https://joeac.net/rss.xml</a>,
|
||||||
|
and I've also got one for just my blog posts at
|
||||||
|
<a href='https://joeac.net/blog/rss.xml'>https://joeac.net/blog/rss.xml</a>,
|
||||||
|
and one just for microlog posts at
|
||||||
|
<a href='https://joeac.net/microlog/rss.xml'>https://joeac.net/microlog/rss.xml</a>.
|
||||||
|
Since computers can understand it, you can get software to read it automatically
|
||||||
|
and tell you when I've made a new blog post. That's much less hassle than having
|
||||||
|
to check manually yourself!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
I'm not the only person serving RSS files, of course. You might not have heard
|
||||||
|
of them, but they're actually all over the Web. In the early days of the Web,
|
||||||
|
everyone was using them, so they've ended up built in to a whole load of
|
||||||
|
foundational Web technology, and hence ubiquitous, even if most Web users have
|
||||||
|
forgotten they exist. Once you've subscribed to my logs, you can subscribe to
|
||||||
|
basically any blog, podcast, or YouTube channel, and a lot of other Websites for
|
||||||
|
news, organisations, whatever. Point a well-designed RSS aggregator or RSS
|
||||||
|
reader at most Websites, and it'll automatically find an associated RSS feed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you're not persuaded, try reading
|
||||||
|
<a href='https://doctorow.medium.com/you-should-be-using-an-rss-reader-76aed31151f9'>
|
||||||
|
Cory Doctorow explaining why RSS is ridiculously good
|
||||||
|
</a>. He's terrifyingly persuasive. Also, read his novels!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you just want to give it a go quickly, probably the quickest way is to
|
||||||
|
use someone else's RSS aggregator that they'll let you use as a service.
|
||||||
|
<a href='https://feedly.com/news-reader'>Feedly</a> and
|
||||||
|
<a href='https://www.inoreader.com'>Inoreader</a> have free tiers.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you want to have a little more control, you can self-host an RSS
|
||||||
|
reader, like <a href='https://tt-rss.org'>Tiny Tiny RSS</a>,
|
||||||
|
<a href='https://miniflux.app'>Miniflux</a>, or
|
||||||
|
<a href='https://freshrss.org'>FreshRSS</a>. But why would you do that?
|
||||||
|
Self-hosting is complicated! Why not just use a normal, local app? Here are
|
||||||
|
some options for that you might want to investigate. All these are active
|
||||||
|
free and open-source software projects at the time of writing.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href='https://hyliu.me/fluent-reader'>Fluent Reader</a> (Linux, Windows, Mac, also mobile I think?)</li>
|
||||||
|
<li><a href='https://www.rssowl.org'>RSSOwl</a> (Linux, Windows, Mac)</li>
|
||||||
|
<li><a href='https://capyreader.com'>Capy Reader</a> (Android)</li>
|
||||||
|
<li><a href='https://apps.kde.org/en-gb/akregator'>Akregator</a> (Linux, Windows (unstable))</li>
|
||||||
|
<li><a href='https://gfeeds.gabmus.org'>GFeeds</a> (Linux)</li>
|
||||||
|
<li><a href='https://lzone.de/liferea'>Liferea</a> (Linux)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
For the terminally geeky, there is also
|
||||||
|
<a href='https://newsboat.org'>Newsboat</a>. I used it for ages, it works
|
||||||
|
fabulously, but it's all in the terminal, so you have to be OK with that.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You can also just enter 'RSS feed reader' into your favourite search
|
||||||
|
engine, software distribution, or app store and see what comes up. Try
|
||||||
|
adding 'FOSS' into the query to prioritise free, open-source software.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
FROM node:lts-alpine3.22
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN apk add --no-cache git
|
|
||||||
COPY website/package.json website/package-lock.json ./
|
|
||||||
RUN npm install && apk del git
|
|
||||||
|
|
||||||
COPY website/astro.config.mjs website/redirects.mjs ./
|
|
||||||
COPY website/db ./db/
|
|
||||||
ARG DB_URL=file:/app/db.sqlite
|
|
||||||
ENV ASTRO_DB_REMOTE_URL=$DB_URL
|
|
||||||
RUN mkdir -p "$(dirname "$(echo "$ASTRO_DB_REMOTE_URL" | cut -d':' -f 2)")"
|
|
||||||
RUN npm run astro db push
|
|
||||||
|
|
||||||
COPY website .
|
|
||||||
|
|
||||||
RUN mkdir -p /common
|
|
||||||
COPY common /common
|
|
||||||
|
|
||||||
ENV HOST=0.0.0.0
|
|
||||||
ENV PORT=4321
|
|
||||||
EXPOSE 4321
|
|
||||||
|
|
||||||
ARG LOCAL_SMTP_HOST
|
|
||||||
ARG LOCAL_SMTP_PASSWORD
|
|
||||||
ARG LOCAL_SMTP_PORT
|
|
||||||
|
|
||||||
RUN npm run build && rm -rf public src
|
|
||||||
|
|
||||||
CMD ["node", "./dist/server/entry.mjs"]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
.astro/
|
|
||||||
dist/
|
|
||||||
node_modules/
|
|
||||||
.dockerignore
|
|
||||||
*.sqlite
|
|
||||||
Dockerfile
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { defineConfig, envField, passthroughImageService } from "astro/config";
|
|
||||||
import db from "@astrojs/db";
|
|
||||||
import gemtext from "astro-gemtext";
|
|
||||||
import mdx from "@astrojs/mdx";
|
|
||||||
import node from "@astrojs/node";
|
|
||||||
import sitemap from "@astrojs/sitemap";
|
|
||||||
|
|
||||||
import redirects from "./redirects.mjs";
|
|
||||||
|
|
||||||
// https://astro.build/config
|
|
||||||
export default defineConfig({
|
|
||||||
adapter: node({
|
|
||||||
mode: "standalone",
|
|
||||||
}),
|
|
||||||
env: {
|
|
||||||
schema: {
|
|
||||||
MAX_DAILY_EMAILS: envField.number({
|
|
||||||
context: "server",
|
|
||||||
access: "secret",
|
|
||||||
}),
|
|
||||||
LOCAL_SMTP_ENVELOPE_FROM: envField.string({
|
|
||||||
context: "server",
|
|
||||||
access: "secret",
|
|
||||||
}),
|
|
||||||
LOCAL_SMTP_HOST: envField.string({ context: "server", access: "secret" }),
|
|
||||||
LOCAL_SMTP_PORT: envField.number({ context: "server", access: "secret" }),
|
|
||||||
LOCAL_SMTP_USER: envField.string({ context: "server", access: "secret" }),
|
|
||||||
LOCAL_SMTP_PASSWORD: envField.string({
|
|
||||||
context: "server",
|
|
||||||
access: "secret",
|
|
||||||
}),
|
|
||||||
CONTACT_MAILBOX: envField.string({ context: "server", access: "secret" }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
service: passthroughImageService(),
|
|
||||||
},
|
|
||||||
redirects,
|
|
||||||
site: "https://joeac.net",
|
|
||||||
integrations: [db(), gemtext(), mdx(), sitemap()],
|
|
||||||
});
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { column, defineDb, defineTable } from "astro:db";
|
|
||||||
|
|
||||||
const Otp = defineTable({
|
|
||||||
columns: {
|
|
||||||
userId: column.text(),
|
|
||||||
value: column.text(),
|
|
||||||
createdAt: column.number(),
|
|
||||||
validUntil: column.number(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const SendmailToken = defineTable({
|
|
||||||
columns: {
|
|
||||||
userId: column.text(),
|
|
||||||
value: column.text(),
|
|
||||||
createdAt: column.number(),
|
|
||||||
validUntil: column.number(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const SentEmails = defineTable({
|
|
||||||
columns: {
|
|
||||||
messageId: column.text(),
|
|
||||||
sentAt: column.number(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default defineDb({
|
|
||||||
tables: {
|
|
||||||
Otp,
|
|
||||||
SendmailToken,
|
|
||||||
SentEmails,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { db } from "astro:db";
|
|
||||||
|
|
||||||
export default async function seed() {}
|
|
||||||
Generated
-8737
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"private": "true",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "astro dev",
|
|
||||||
"start": "astro build --remote && node ./dist/server/entry.mjs",
|
|
||||||
"build": "astro build --remote",
|
|
||||||
"preview": "astro preview",
|
|
||||||
"sync": "astro sync",
|
|
||||||
"astro": "astro"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^22.14.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@astrojs/db": "^0.20.1",
|
|
||||||
"@astrojs/mdx": "^5.0.2",
|
|
||||||
"@astrojs/node": "^10.0.3",
|
|
||||||
"@astrojs/rss": "^4.0.17",
|
|
||||||
"@astrojs/sitemap": "^3.7.1",
|
|
||||||
"@types/nodemailer": "^7.0.4",
|
|
||||||
"astro": "^6.0.8",
|
|
||||||
"astro-gemtext": "git+https://git.joeac.net/joeac/astro-gemtext.git#ab32140ae28ff451447e7b25e24de0a4d46d264d",
|
|
||||||
"gemtext": "^0.2.2",
|
|
||||||
"markdown-it": "^14.1.0",
|
|
||||||
"nodemailer": "^7.0.11",
|
|
||||||
"typescript": "^5.4.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@astrojs/check": "^0.9.8",
|
|
||||||
"@types/markdown-it": "^14.1.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,516 +0,0 @@
|
|||||||
/** Variables */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--colour-primary-10: #060300;
|
|
||||||
--colour-primary-15: #150800;
|
|
||||||
--colour-primary-20: #1f1400;
|
|
||||||
--colour-primary-30: #3c2b00;
|
|
||||||
--colour-primary-40: #5c4300;
|
|
||||||
--colour-primary-50: #7f5d00;
|
|
||||||
--colour-primary-60: #a37800;
|
|
||||||
--colour-primary-70: #c89500;
|
|
||||||
--colour-primary-80: #efb300;
|
|
||||||
--colour-primary-90: #ffd98c;
|
|
||||||
--colour-primary-95: #ffecc8;
|
|
||||||
|
|
||||||
--colour-hyperlink-10: #000409;
|
|
||||||
--colour-hyperlink-20: #001829;
|
|
||||||
--colour-hyperlink-30: #00314d;
|
|
||||||
--colour-hyperlink-40: #004d75;
|
|
||||||
--colour-hyperlink-50: #006a9f;
|
|
||||||
--colour-hyperlink-60: #1388c9;
|
|
||||||
--colour-hyperlink-70: #41a8ea;
|
|
||||||
--colour-hyperlink-80: #78c7ff;
|
|
||||||
--colour-hyperlink-90: #bfe3ff;
|
|
||||||
--colour-hyperlink-95: #e0f1ff;
|
|
||||||
|
|
||||||
--colour-grey-10: oklch(0.1 0.01 84);
|
|
||||||
--colour-grey-20: oklch(0.2 0.01 84);
|
|
||||||
--colour-grey-30: oklch(0.3 0.01 84);
|
|
||||||
--colour-grey-40: oklch(0.4 0.01 84);
|
|
||||||
--colour-grey-50: oklch(0.5 0.01 84);
|
|
||||||
--colour-grey-60: oklch(0.6 0.01 84);
|
|
||||||
--colour-grey-70: oklch(0.7 0.01 84);
|
|
||||||
--colour-grey-80: oklch(0.8 0.01 84);
|
|
||||||
--colour-grey-90: oklch(0.9 0.01 84);
|
|
||||||
--colour-grey-95: oklch(0.95 0.01 84);
|
|
||||||
|
|
||||||
--colour-error-10: oklch(0.1 0.2 26);
|
|
||||||
--colour-error-20: oklch(0.2 0.2 26);
|
|
||||||
--colour-error-30: oklch(0.3 0.2 26);
|
|
||||||
--colour-error-40: oklch(0.4 0.2 26);
|
|
||||||
--colour-error-50: oklch(0.5 0.2 26);
|
|
||||||
--colour-error-60: oklch(0.6 0.2 26);
|
|
||||||
--colour-error-70: oklch(0.7 0.2 26);
|
|
||||||
--colour-error-80: oklch(0.8 0.2 26);
|
|
||||||
--colour-error-90: oklch(0.9 0.2 26);
|
|
||||||
--colour-error-95: oklch(0.95 0.2 26);
|
|
||||||
|
|
||||||
--colour-warn-10: oklch(0.1 0.2 46);
|
|
||||||
--colour-warn-20: oklch(0.2 0.2 46);
|
|
||||||
--colour-warn-30: oklch(0.3 0.2 46);
|
|
||||||
--colour-warn-40: oklch(0.4 0.2 46);
|
|
||||||
--colour-warn-50: oklch(0.5 0.2 46);
|
|
||||||
--colour-warn-60: oklch(0.6 0.2 46);
|
|
||||||
--colour-warn-70: oklch(0.7 0.2 46);
|
|
||||||
--colour-warn-80: oklch(0.8 0.2 46);
|
|
||||||
--colour-warn-90: oklch(0.9 0.2 46);
|
|
||||||
--colour-warn-95: oklch(0.95 0.2 46);
|
|
||||||
|
|
||||||
--colour-primary-fg: var(--colour-primary-90);
|
|
||||||
--colour-primary-fg-accent: var(--colour-primary-80);
|
|
||||||
--colour-primary-bg: var(--colour-primary-10);
|
|
||||||
--colour-primary-bg-accent: var(--colour-primary-20);
|
|
||||||
--colour-code-fg: var(--colour-primary-90);
|
|
||||||
--colour-code-bg: var(--colour-primary-15);
|
|
||||||
--colour-hyperlink: var(--colour-hyperlink-80);
|
|
||||||
--colour-grey-fg: var(--colour-grey-70);
|
|
||||||
--colour-grey-bg: var(--colour-grey-30);
|
|
||||||
--colour-error-fg: var(--colour-error-90);
|
|
||||||
--colour-error-bg: var(--colour-error-40);
|
|
||||||
--colour-warn-fg: var(--colour-warn-20);
|
|
||||||
--colour-warn-bg: var(--colour-warn-80);
|
|
||||||
|
|
||||||
--font-size-sm: 1rem;
|
|
||||||
--font-size-base: 1.125rem;
|
|
||||||
--font-size-md: 1.5rem;
|
|
||||||
--font-size-lg: 2rem;
|
|
||||||
--font-size-xl: 3rem;
|
|
||||||
|
|
||||||
--spacing-block-xs: 0.5rem;
|
|
||||||
--spacing-block-sm: 1.75rem;
|
|
||||||
--spacing-block-md: 2.5rem;
|
|
||||||
--spacing-block-lg: 3.5rem;
|
|
||||||
--spacing-block-xl: 5rem;
|
|
||||||
--spacing-inline-xs: 0.25rem;
|
|
||||||
--spacing-inline-sm: 0.5rem;
|
|
||||||
--spacing-inline-md: 1.5rem;
|
|
||||||
--spacing-inline-lg: 3rem;
|
|
||||||
--spacing-inline-xl: 6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Light theme */
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
--colour-primary-fg: var(--colour-primary-20);
|
|
||||||
--colour-primary-fg-accent: var(--colour-primary-40);
|
|
||||||
--colour-primary-bg: var(--colour-primary-95);
|
|
||||||
--colour-hyperlink: var(--colour-hyperlink-40);
|
|
||||||
--colour-grey-fg: var(--colour-grey-40);
|
|
||||||
--colour-grey-bg: var(--colour-grey-80);
|
|
||||||
--colour-error-fg: var(--colour-error-20);
|
|
||||||
--colour-error-bg: var(--colour-error-80);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Base typography */
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
color: var(--colour-primary-fg);
|
|
||||||
font-weight: light;
|
|
||||||
background-color: var(--colour-primary-bg);
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
/* Geometric Humanist stack from https://modernfontstacks.com */
|
|
||||||
font-family:
|
|
||||||
Avenir, Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol, blockquote) {
|
|
||||||
margin-block-start: var(--spacing-block-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
height: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Base layout */
|
|
||||||
|
|
||||||
body {
|
|
||||||
--body-margin-inline-start: var(--spacing-inline-lg);
|
|
||||||
--body-margin-inline-end: var(--body-margin-inline-start);
|
|
||||||
--body-margin-block-end: var(--spacing-block-xl);
|
|
||||||
margin-inline: var(--body-margin-inline-start) var(--body-margin-inline-end);
|
|
||||||
margin-block-end: var(--body-margin-block-end);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(h1, h2, h3, h4, h5, h6) {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
margin-inline: auto;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 60rem) {
|
|
||||||
body {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns:
|
|
||||||
[grid-start media-start]
|
|
||||||
var(--grid-margin-inline)
|
|
||||||
[media-end content-start]
|
|
||||||
minmax(var(--grid-max-content-width), auto)
|
|
||||||
[content-end grid-end];
|
|
||||||
grid-auto-rows: max-content;
|
|
||||||
column-gap: var(--spacing-block-sm);
|
|
||||||
max-width: var(--grid-total-width);
|
|
||||||
|
|
||||||
--body-margin-inline-end: 6rem;
|
|
||||||
--grid-margin-inline: 6rem;
|
|
||||||
--grid-total-width: 48rem;
|
|
||||||
--grid-max-content-width: calc(
|
|
||||||
var(--grid-total-width) - var(--body-margin-inline-start) -
|
|
||||||
var(--grid-margin-inline) - var(--spacing-block-sm) -
|
|
||||||
var(--grid-margin-inline)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(main, article, nav) {
|
|
||||||
display: grid;
|
|
||||||
grid-column: grid;
|
|
||||||
grid-template-columns: subgrid;
|
|
||||||
|
|
||||||
> :is(section, header, aside, form) {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: subgrid;
|
|
||||||
grid-column: grid;
|
|
||||||
|
|
||||||
> :not(.not-grid-content) {
|
|
||||||
grid-column: content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(h1, h2, h3, h4, h5, h6) {
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 80rem) {
|
|
||||||
body {
|
|
||||||
grid-template-columns:
|
|
||||||
[grid-start media-start]
|
|
||||||
var(--grid-margin-inline)
|
|
||||||
[media-end content-start]
|
|
||||||
minmax(auto, var(--grid-max-content-width))
|
|
||||||
[content-end margin-start]
|
|
||||||
auto
|
|
||||||
[margin-end grid-end];
|
|
||||||
|
|
||||||
--grid-total-width: 80rem;
|
|
||||||
--grid-max-content-width: 40rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Headings */
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
font-weight: 900;
|
|
||||||
margin-block-start: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
font-weight: 900;
|
|
||||||
margin-block-start: var(--spacing-block-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-block-start: var(--spacing-block-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Hyperlinks */
|
|
||||||
|
|
||||||
a:is(:link, :visited) {
|
|
||||||
color: var(--colour-hyperlink);
|
|
||||||
text-decoration: underline;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: wavy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Definition lists */
|
|
||||||
dl {
|
|
||||||
margin-block-start: var(--spacing-block-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
dl dt {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl dd + dt {
|
|
||||||
margin-block-start: var(--spacing-block-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** figcaptions */
|
|
||||||
|
|
||||||
figcaption {
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
font-style: italic;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Lists */
|
|
||||||
|
|
||||||
:is(ol, ul) {
|
|
||||||
margin-inline-start: var(--spacing-inline-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Navigation bar */
|
|
||||||
|
|
||||||
nav {
|
|
||||||
margin-block: var(--spacing-block-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
grid-column: media-start / content-end;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--spacing-inline-md);
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 36rem) {
|
|
||||||
nav {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: subgrid;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
justify-content: end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Emphasis */
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Blog feed */
|
|
||||||
|
|
||||||
.h-feed ul {
|
|
||||||
list-style: none;
|
|
||||||
margin-inline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Block quotes */
|
|
||||||
blockquote {
|
|
||||||
padding-inline-start: var(--spacing-inline-lg);
|
|
||||||
border-inline-start: 2px solid var(--colour-primary-fg);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote footer {
|
|
||||||
font-style: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote :is(b, strong) {
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote :is(i, em) {
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Small caps */
|
|
||||||
.small-caps {
|
|
||||||
font-variant: small-caps;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Pre-formatted blocks */
|
|
||||||
pre {
|
|
||||||
border: 2px solid var(--colour-primary-fg);
|
|
||||||
border-radius: 2px;
|
|
||||||
background-color: var(--colour-code-bg) !important;
|
|
||||||
margin-block-start: var(--spacing-block-sm);
|
|
||||||
padding-inline: var(--spacing-inline-sm);
|
|
||||||
padding-block: var(--spacing-block-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Code blocks */
|
|
||||||
code {
|
|
||||||
border: 2px solid var(--colour-primary-fg);
|
|
||||||
border-radius: 2px;
|
|
||||||
padding-inline: var(--spacing-inline-xs);
|
|
||||||
color: var(--colour-code-fg);
|
|
||||||
background-color: var(--colour-code-bg);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
}
|
|
||||||
pre code {
|
|
||||||
border: none;
|
|
||||||
border-radius: none;
|
|
||||||
padding: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* verse */
|
|
||||||
.verse {
|
|
||||||
span {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.verse--hanging-indents {
|
|
||||||
span + span:not(.not-hanging) {
|
|
||||||
margin-inline-start: var(--spacing-inline-md);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* block-comment */
|
|
||||||
block-comment {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: subgrid;
|
|
||||||
grid-column: grid;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
grid-column: content;
|
|
||||||
}
|
|
||||||
|
|
||||||
> blockquote:last-of-type {
|
|
||||||
padding-block: var(--spacing-block-sm);
|
|
||||||
padding-inline: var(--spacing-inline-md);
|
|
||||||
overflow-y: scroll;
|
|
||||||
border: 2px solid var(--colour-primary-fg);
|
|
||||||
--colour-scroll-shadow: color-mix(
|
|
||||||
in srgb,
|
|
||||||
var(--colour-primary-fg),
|
|
||||||
transparent 20%
|
|
||||||
);
|
|
||||||
|
|
||||||
background:
|
|
||||||
linear-gradient(var(--colour-primary-bg) 30%, transparent) center top,
|
|
||||||
linear-gradient(transparent, var(--colour-primary-bg) 70%) center bottom,
|
|
||||||
radial-gradient(
|
|
||||||
farthest-side at 50% 0,
|
|
||||||
var(--colour-scroll-shadow),
|
|
||||||
transparent
|
|
||||||
)
|
|
||||||
center top,
|
|
||||||
radial-gradient(
|
|
||||||
farthest-side at 50% 100%,
|
|
||||||
var(--colour-scroll-shadow),
|
|
||||||
transparent
|
|
||||||
)
|
|
||||||
center bottom;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size:
|
|
||||||
100% 2rem,
|
|
||||||
100% 2rem,
|
|
||||||
100% 1rem,
|
|
||||||
100% 1rem;
|
|
||||||
background-attachment: local, local, scroll, scroll;
|
|
||||||
|
|
||||||
> :first-child {
|
|
||||||
margin-block-start: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 80rem) {
|
|
||||||
block-comment > blockquote:last-of-type {
|
|
||||||
grid-column: margin;
|
|
||||||
max-height: 67vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* forms */
|
|
||||||
form {
|
|
||||||
margin-inline: auto;
|
|
||||||
max-width: max-content;
|
|
||||||
|
|
||||||
:is(button, fieldset, input, label, object, output, select, textarea, img) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin-block-end: var(--spacing-block-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
* + :is(label, input[type="submit"], button) {
|
|
||||||
margin-block-start: var(--spacing-block-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(input[type="submit"], input[type="button"], button) {
|
|
||||||
padding-inline: var(--spacing-inline-sm);
|
|
||||||
max-width: max-content;
|
|
||||||
&:not(dialog *) {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(input[type="email"], input[type="text"], select) {
|
|
||||||
max-width: 100%;
|
|
||||||
width: 16rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
max-width: 100%;
|
|
||||||
width: 40rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* dialogs */
|
|
||||||
|
|
||||||
dialog {
|
|
||||||
background: var(--colour-primary-bg-accent);
|
|
||||||
border: 2px solid var(--colour-primary-fg-accent);
|
|
||||||
bottom: auto;
|
|
||||||
color: var(--colour-primary-fg);
|
|
||||||
left: calc(0.5 * (100vw - min(90vw, 36rem)));
|
|
||||||
margin: 0;
|
|
||||||
padding-block: var(--spacing-block-sm);
|
|
||||||
padding-inline: var(--spacing-inline-sm);
|
|
||||||
text-align: center;
|
|
||||||
top: auto;
|
|
||||||
width: min(90vw, 36rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 80rem) {
|
|
||||||
dialog {
|
|
||||||
left: calc(
|
|
||||||
var(--body-margin-inline-start) + var(--grid-margin-inline) + 2 *
|
|
||||||
var(--spacing-inline-md)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* utilities */
|
|
||||||
:is(
|
|
||||||
.para-spacing-tight:is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol),
|
|
||||||
.para-spacing-tight :is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol)
|
|
||||||
) {
|
|
||||||
margin-block-start: var(--spacing-block-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background: var(--colour-error-bg);
|
|
||||||
border: 2px solid var(--colour-error-fg);
|
|
||||||
color: var(--colour-error-fg);
|
|
||||||
padding-block: var(--spacing--block-sm);
|
|
||||||
padding-inline: var(--spacing-inline-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.warn {
|
|
||||||
background: var(--colour-warn-bg);
|
|
||||||
border: 2px solid var(--colour-warn-fg);
|
|
||||||
color: var(--colour-warn-fg);
|
|
||||||
padding-block: var(--spacing--block-sm);
|
|
||||||
padding-inline: var(--spacing-inline-sm);
|
|
||||||
--colour-hyperlink: var(--colour-hyperlink-40);
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
div:has(img) {
|
|
||||||
width: 6rem;
|
|
||||||
height: 6rem;
|
|
||||||
margin-inline: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
div img {
|
|
||||||
width: 6rem;
|
|
||||||
height: 6rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
filter: contrast(1.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
div:has(img)::after {
|
|
||||||
/* Colour overlay */
|
|
||||||
background-color: var(--colour-primary-80);
|
|
||||||
opacity: 0.3;
|
|
||||||
|
|
||||||
/* Same size and shape as the img */
|
|
||||||
border-radius: 1rem;
|
|
||||||
width: 6rem;
|
|
||||||
height: 6rem;
|
|
||||||
|
|
||||||
/* Positioned on top of the img */
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
top: -6rem;
|
|
||||||
|
|
||||||
/* A content value is needed to get the ::after to render */
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
text-align: center;
|
|
||||||
border-block-end: 1px solid var(--colour-primary-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
grid-column: 1 / 3;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.technical-skills h3 {
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.technical-skills ul {
|
|
||||||
color: var(--colour-primary-fg-accent);
|
|
||||||
margin-inline-start: var(--spacing-inline-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 46rem) {
|
|
||||||
.technical-skills section {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-inline-sm);
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.technical-skills section h3::after {
|
|
||||||
content: '/';
|
|
||||||
margin-inline: var(--spacing-inline-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.technical-skills section ul {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-inline-sm);
|
|
||||||
list-style: none;
|
|
||||||
margin-inline-start: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.technical-skills section ul li + li::before {
|
|
||||||
content: '•';
|
|
||||||
margin-inline-end: var(--spacing-inline-sm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(.experience, .passions) :is(ol, ul) {
|
|
||||||
list-style: none;
|
|
||||||
margin-inline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(.experience, .passions) :is(ol, ul) li {
|
|
||||||
margin-block-start: var(--spacing-block-sm);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/* Assumes there is at most one level of subheading for sub-dividing entries */
|
|
||||||
.h-feed :is(h2, h3, h4, h5, h6) {
|
|
||||||
margin-block-start: var(--spacing-block-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-feed .h-entry {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-feed .h-entry + .h-entry {
|
|
||||||
margin-block-start: var(--spacing-block-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-feed .h-entry > * {
|
|
||||||
order: 1;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-feed .h-entry .dt-published {
|
|
||||||
order: 0;
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-feed .h-entry .p-name {
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-feed .full-feed-link {
|
|
||||||
text-align: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-feed :is(a.full-feed-link, .full-feed-link a)::after {
|
|
||||||
content: " >";
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
.h-card div:has(img) {
|
|
||||||
width: 6rem;
|
|
||||||
height: 6rem;
|
|
||||||
margin-inline: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-card img {
|
|
||||||
width: 6rem;
|
|
||||||
height: 6rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
filter: contrast(1.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-card div:has(img)::after {
|
|
||||||
/* Colour overlay */
|
|
||||||
background-color: var(--colour-primary-80);
|
|
||||||
opacity: 0.3;
|
|
||||||
|
|
||||||
/* Same size and shape as the img */
|
|
||||||
border-radius: 1rem;
|
|
||||||
width: 6rem;
|
|
||||||
height: 6rem;
|
|
||||||
|
|
||||||
/* Positioned on top of the img */
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
top: -6rem;
|
|
||||||
|
|
||||||
/* A content value is needed to get the ::after to render */
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 36rem) {
|
|
||||||
.h-card {
|
|
||||||
grid-column: media-start / content-end;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: subgrid; /** Subgrid of main column layout */
|
|
||||||
grid-template-rows: min-content 1fr;
|
|
||||||
grid-template-areas:
|
|
||||||
"empty heading"
|
|
||||||
"photo text";
|
|
||||||
column-gap: var(--spacing-block-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-card div:has(img) {
|
|
||||||
grid-area: photo;
|
|
||||||
margin-block-start: var(--spacing-block-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-card header {
|
|
||||||
grid-area: heading;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-card__text {
|
|
||||||
grid-area: text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
.otp-inputs {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--spacing-inline-sm);
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
input {
|
|
||||||
border: none;
|
|
||||||
border-block-end: 2px solid var(--colour-grey-fg);
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
text-align: center;
|
|
||||||
width: var(--font-size-lg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.otp-form {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-block-sm);
|
|
||||||
|
|
||||||
> * {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="submit"] {
|
|
||||||
max-width: max-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.p-summary {
|
|
||||||
font-style: italic;
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin-block-start: var(--spacing-block-md);
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
/* Based on Andy Bell’s More Modern CSS Reset: https://piccalil.li/blog/a-more-modern-css-reset/ */
|
|
||||||
|
|
||||||
/* Box sizing rules */
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prevent font size inflation */
|
|
||||||
html {
|
|
||||||
-moz-text-size-adjust: none;
|
|
||||||
-webkit-text-size-adjust: none;
|
|
||||||
text-size-adjust: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove default margin in favour of better control in authored CSS */
|
|
||||||
body,
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
p,
|
|
||||||
figure,
|
|
||||||
blockquote,
|
|
||||||
dl,
|
|
||||||
dd {
|
|
||||||
margin-block: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
|
|
||||||
ul[role="list"],
|
|
||||||
ol[role="list"] {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Set core body defaults */
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Set shorter line heights on headings and interactive elements */
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
button,
|
|
||||||
input,
|
|
||||||
label {
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Balance text wrapping on headings */
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4 {
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make images easier to work with */
|
|
||||||
img,
|
|
||||||
picture {
|
|
||||||
max-width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inherit fonts for inputs and buttons */
|
|
||||||
input,
|
|
||||||
button,
|
|
||||||
textarea,
|
|
||||||
select {
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make sure textareas without a rows attribute are not tiny */
|
|
||||||
textarea:not([rows]) {
|
|
||||||
min-height: 10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Anything that has been anchored to should have extra scroll margin */
|
|
||||||
:target {
|
|
||||||
scroll-margin-block: 5ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../common/images/
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
export default {
|
|
||||||
"/links": "/microlog",
|
|
||||||
"/links/rss.xml": "/microlog/rss.xml",
|
|
||||||
"/blog/2024/10/tracking_pixels": "/blog/2024-04-10",
|
|
||||||
"/blog/2024/01/14/sapiens_on_religion": "/blog/2024-01-14",
|
|
||||||
"/blog/2024/01/29/euhwc_toast_to_the_lasses_2024": "/blog/2024-01-29",
|
|
||||||
"/blog/2024/03/30/easter": "/blog/2024-03-30",
|
|
||||||
"/blog/2024/04/10/tracking_pixels": "/blog/2024-04-10",
|
|
||||||
"/blog/2024/04/11/who_consecrates_the_temple": "/blog/2024-04-11",
|
|
||||||
"/blog/2024/04/14/god_is_not_great": "/blog/2024-04-14",
|
|
||||||
"/blog/2024/05/02/no_more_youtube": "/blog/2024-05-02",
|
|
||||||
"/blog/2024/06/13/llms_do_not_understand_anything": "/blog/2024-06-13",
|
|
||||||
"/blog/2024/07/08/doctor_who_gayness_church": "/blog/2024-07-08",
|
|
||||||
"/blog/2024/07/16/word_hallucination_with_reference_to_llms":
|
|
||||||
"/blog/2024-07-16",
|
|
||||||
"/blog/2024/12/17/open_questions_about_sex": "/blog/2024-12-17",
|
|
||||||
"/blog/2025/01/19/my_feed_and_reading_list": "/blog/2025-01-19",
|
|
||||||
"/blog/2025/01/19/newsboat-post.webp": "/blog/2025-01-19_newsboat-post.webp",
|
|
||||||
"/blog/2025/01/19/newsboat.webp": "/blog/2025-01-19_newsboat.webp",
|
|
||||||
"/blog/2025/01/19/ranger.webp": "/blog/2025-01-19_ranger.webp",
|
|
||||||
"/blog/2025/01/19/w3m.webp": "/blog/2025-01-19_w3m.webp",
|
|
||||||
"/blog/2025/01/24/science_and_philosophy": "/blog/2025-01-24",
|
|
||||||
"/blog/2025/01/28/paradox": "/blog/2025-01-28",
|
|
||||||
"/blog/2025/05/02/surprised_by_hope": "/blog/2025-05-02",
|
|
||||||
"/blog/2025/05/04/does_resurrection_ground_works": "/blog/2025-05-04",
|
|
||||||
"/blog/2025/06/23/work": "/blog/2025-06-23",
|
|
||||||
"/blog/2025/07/03/ps118": "/blog/2025-07-03",
|
|
||||||
"/blog/2025/09/18/starting_msc": "/blog/2025-09-18",
|
|
||||||
"/blog/2025/09/24/creeds": "/blog/2025-09-24",
|
|
||||||
"/blog/2025/10/05/creeds": "/blog/2025-10-05",
|
|
||||||
"/blog/2025/10/09/arius": "/blog/2025-10-09",
|
|
||||||
"/blog/2025/12/11/persecution": "/blog/2025-12-11",
|
|
||||||
"/blog/2026/02/16/short_reformation_stories": "/blog/2026-02-16",
|
|
||||||
"/blog/2026/03/04/agnostic": "/blog/2026-03-04",
|
|
||||||
"/blog/2026/03/21/genesis": "/blog/2026-03-21",
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import otp from "./otp/otp";
|
|
||||||
import sendmail from "./sendmail";
|
|
||||||
|
|
||||||
export const server = {
|
|
||||||
otp,
|
|
||||||
sendmail,
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import send from "./send-otp";
|
|
||||||
import verify from "./verify-otp";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
send,
|
|
||||||
verify,
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import crypto from "node:crypto";
|
|
||||||
import { z } from "astro/zod";
|
|
||||||
import { defineAction } from "astro:actions";
|
|
||||||
import { db, gte, Otp, SentEmails } from "astro:db";
|
|
||||||
import { transporter } from "../sendmail";
|
|
||||||
import { LOCAL_SMTP_ENVELOPE_FROM, MAX_DAILY_EMAILS } from "astro:env/server";
|
|
||||||
|
|
||||||
export default defineAction({
|
|
||||||
input: z.object({
|
|
||||||
email: z.string().email(),
|
|
||||||
name: z.string().optional(),
|
|
||||||
type: z.enum(["email"]),
|
|
||||||
}),
|
|
||||||
handler: sendOtp,
|
|
||||||
});
|
|
||||||
|
|
||||||
type OtpParams = {
|
|
||||||
email: string;
|
|
||||||
name?: string;
|
|
||||||
type: "email";
|
|
||||||
};
|
|
||||||
|
|
||||||
async function sendOtp({ email, name }: OtpParams) {
|
|
||||||
const otp = crypto.randomBytes(3).toString("hex").toLocaleUpperCase();
|
|
||||||
const otpPretty = `${otp.slice(0, 3)}-${otp.slice(3)}`;
|
|
||||||
|
|
||||||
const emailsSentLast24Hours = await db.$count(
|
|
||||||
SentEmails,
|
|
||||||
gte(SentEmails.sentAt, Date.now() - 1000 * 60 * 60 * 24),
|
|
||||||
);
|
|
||||||
if (emailsSentLast24Hours >= MAX_DAILY_EMAILS) {
|
|
||||||
console.warn(
|
|
||||||
`${name} <${email}> requested an OTP, but ${emailsSentLast24Hours} have already been sent, whereas the max daily load is ${MAX_DAILY_EMAILS}.`,
|
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
`${emailsSentLast24Hours} emails have been sent in the last 24 hours, but the max daily load is ${MAX_DAILY_EMAILS}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = await transporter.sendMail({
|
|
||||||
from: LOCAL_SMTP_ENVELOPE_FROM,
|
|
||||||
to: `${name ? `"${name}" ` : ""}<${email}>`,
|
|
||||||
subject: `joeac.net: your OTP is ${otpPretty}`,
|
|
||||||
text: `
|
|
||||||
Someone tried to use this email address on joeac.net. If this was you,
|
|
||||||
your one-time passcode is ${otpPretty}. If this wasn't you, you don't need
|
|
||||||
to do anything.`,
|
|
||||||
});
|
|
||||||
console.log(
|
|
||||||
`Sent OTP (${otpPretty}) to ${email}. Message ID: ${info.messageId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await db
|
|
||||||
.insert(SentEmails)
|
|
||||||
.values({ messageId: info.messageId, sentAt: Date.now() });
|
|
||||||
|
|
||||||
await db.insert(Otp).values({
|
|
||||||
userId: email,
|
|
||||||
value: otp,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
validUntil: Date.now() + 1000 * 60 * 5,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
|
||||||
import { z } from "astro/zod";
|
|
||||||
import { defineAction } from "astro:actions";
|
|
||||||
import { and, db, eq, gte, Otp, SendmailToken } from "astro:db";
|
|
||||||
|
|
||||||
export default defineAction({
|
|
||||||
input: z.object({
|
|
||||||
guess: z.string().length(6),
|
|
||||||
lenient: z.boolean().default(false),
|
|
||||||
userId: z.string().nonempty(),
|
|
||||||
}),
|
|
||||||
handler: verifyOtp,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function verifyOtp({ guess, lenient, userId }: VerifyOtpParams) {
|
|
||||||
const leniency = lenient ? 1000 * 60 : 0;
|
|
||||||
const isOtpCorrect =
|
|
||||||
(await db.$count(
|
|
||||||
Otp,
|
|
||||||
and(
|
|
||||||
eq(Otp.userId, userId),
|
|
||||||
eq(Otp.value, guess),
|
|
||||||
gte(Otp.validUntil, Date.now() - leniency),
|
|
||||||
),
|
|
||||||
)) > 0;
|
|
||||||
|
|
||||||
if (!isOtpCorrect) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.delete(Otp).where(and(eq(Otp.userId, userId), eq(Otp.value, guess)));
|
|
||||||
|
|
||||||
const token = randomBytes(256).toString("hex");
|
|
||||||
await db.insert(SendmailToken).values({
|
|
||||||
userId,
|
|
||||||
value: token,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
validUntil: Date.now() + 60_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
type VerifyOtpParams = {
|
|
||||||
guess: string;
|
|
||||||
lenient: boolean;
|
|
||||||
userId: string;
|
|
||||||
};
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { z } from "astro/zod";
|
|
||||||
import { defineAction } from "astro:actions";
|
|
||||||
import nodemailer from "nodemailer";
|
|
||||||
import {
|
|
||||||
MAX_DAILY_EMAILS,
|
|
||||||
LOCAL_SMTP_HOST,
|
|
||||||
LOCAL_SMTP_PASSWORD,
|
|
||||||
LOCAL_SMTP_PORT,
|
|
||||||
LOCAL_SMTP_USER,
|
|
||||||
CONTACT_MAILBOX,
|
|
||||||
LOCAL_SMTP_ENVELOPE_FROM,
|
|
||||||
} from "astro:env/server";
|
|
||||||
import { and, db, eq, gte, SendmailToken, SentEmails } from "astro:db";
|
|
||||||
|
|
||||||
export default defineAction({
|
|
||||||
input: z.object({
|
|
||||||
email: z.string().email(),
|
|
||||||
message: z.string().nonempty(),
|
|
||||||
name: z.string().nonempty(),
|
|
||||||
userId: z.string().nonempty(),
|
|
||||||
token: z.string().nonempty(),
|
|
||||||
}),
|
|
||||||
handler: sendmail,
|
|
||||||
});
|
|
||||||
|
|
||||||
type SendEmailParams = {
|
|
||||||
email: string;
|
|
||||||
message: string;
|
|
||||||
name: string;
|
|
||||||
token: string;
|
|
||||||
userId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function sendmail({
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
message,
|
|
||||||
token,
|
|
||||||
userId,
|
|
||||||
}: SendEmailParams) {
|
|
||||||
const isTokenCorrect =
|
|
||||||
(await db.$count(
|
|
||||||
SendmailToken,
|
|
||||||
and(
|
|
||||||
eq(SendmailToken.userId, userId),
|
|
||||||
eq(SendmailToken.value, token),
|
|
||||||
gte(SendmailToken.validUntil, Date.now()),
|
|
||||||
),
|
|
||||||
)) > 0;
|
|
||||||
if (!isTokenCorrect) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await db
|
|
||||||
.delete(SendmailToken)
|
|
||||||
.where(
|
|
||||||
and(eq(SendmailToken.userId, userId), eq(SendmailToken.value, token)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const emailsSentLast24Hours = await db.$count(
|
|
||||||
SentEmails,
|
|
||||||
gte(SentEmails.sentAt, Date.now() - 1000 * 60 * 60 * 24),
|
|
||||||
);
|
|
||||||
if (emailsSentLast24Hours > MAX_DAILY_EMAILS) {
|
|
||||||
console.warn(
|
|
||||||
`${name} <${email}> tried to send an email, but ${emailsSentLast24Hours} have already been sent, whereas the max daily load is ${MAX_DAILY_EMAILS}.`,
|
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
`${emailsSentLast24Hours} emails have been sent in the last 24 hours, but the max daily load is ${MAX_DAILY_EMAILS}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = await transporter.sendMail({
|
|
||||||
from: LOCAL_SMTP_ENVELOPE_FROM,
|
|
||||||
to: CONTACT_MAILBOX,
|
|
||||||
subject: `joeac.net: ${name} left a message`,
|
|
||||||
text: `${name} <${email}> sent you a message:\n\n\n${message}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db
|
|
||||||
.insert(SentEmails)
|
|
||||||
.values({ messageId: info.messageId, sentAt: Date.now() });
|
|
||||||
|
|
||||||
console.log("Sent an email to Joe. Message ID: ", info.messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const transporter = nodemailer.createTransport({
|
|
||||||
host: LOCAL_SMTP_HOST,
|
|
||||||
from: LOCAL_SMTP_ENVELOPE_FROM,
|
|
||||||
port: LOCAL_SMTP_PORT,
|
|
||||||
secure: false,
|
|
||||||
authMethod: "PLAIN",
|
|
||||||
auth: {
|
|
||||||
type: "login",
|
|
||||||
user: LOCAL_SMTP_USER,
|
|
||||||
pass: LOCAL_SMTP_PASSWORD,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
---
|
|
||||||
interface Props {
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
image?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
|
||||||
|
|
||||||
const { title, description, image = '/images/headshot.jpg' } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- IndieWeb -->
|
|
||||||
<link rel="authorization_endpoint" href="https://indieauth.com/auth">
|
|
||||||
<link rel="token_endpoint" href="https://tokens.indieauth.com/token">
|
|
||||||
<link rel="micropub" href="https://tasty-windows-lick.loca.lt">
|
|
||||||
|
|
||||||
<!-- Stylesheets -->
|
|
||||||
<link rel="stylesheet" href="/css/reset.css" />
|
|
||||||
<link rel="stylesheet" href="/css/base.css" />
|
|
||||||
<link rel="stylesheet" href="/css/hcard.css" />
|
|
||||||
<link rel="stylesheet" href="/css/feed.css" />
|
|
||||||
|
|
||||||
<!-- Global Metadata -->
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<meta name="generator" content={Astro.generator} />
|
|
||||||
|
|
||||||
<!-- Canonical URL -->
|
|
||||||
<link rel="canonical" href={canonicalURL} />
|
|
||||||
|
|
||||||
<!-- Primary Meta Tags -->
|
|
||||||
<title>{title}</title>
|
|
||||||
<meta name="title" content={title} />
|
|
||||||
<meta name="description" content={description} />
|
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:url" content={Astro.url} />
|
|
||||||
<meta property="og:title" content={title} />
|
|
||||||
<meta property="og:description" content={description} />
|
|
||||||
<meta property="og:image" content={new URL(image, Astro.url)} />
|
|
||||||
|
|
||||||
<!-- Twitter -->
|
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
|
||||||
<meta property="twitter:url" content={Astro.url} />
|
|
||||||
<meta property="twitter:title" content={title} />
|
|
||||||
<meta property="twitter:description" content={description} />
|
|
||||||
<meta property="twitter:image" content={new URL(image, Astro.url)} />
|
|
||||||
|
|
||||||
<!-- Feeds -->
|
|
||||||
<link rel="alternate" type="text/xml" title="Blog RSS" href="/blog/rss.xml">
|
|
||||||
<link rel="alternate" type="text/xml" title="Links RSS" href="/links/rss.xml">
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
---
|
|
||||||
import type { CollectionEntry } from 'astro:content';
|
|
||||||
import { getCollection } from 'astro:content';
|
|
||||||
import FormattedDate from './FormattedDate.astro';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6,
|
|
||||||
hideAuthor?: boolean,
|
|
||||||
hideSubheadings?: boolean,
|
|
||||||
maxEntries?: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { headingLevel = 2, hideSubheadings = false, hideAuthor = false, maxEntries } = Astro.props;
|
|
||||||
|
|
||||||
const allPosts = (await getCollection('blog')).filter((post) => !post.data.hidden);
|
|
||||||
|
|
||||||
const posts = maxEntries === undefined
|
|
||||||
? allPosts
|
|
||||||
: allPosts.sort(sortByPubDateDescending).slice(0, maxEntries);
|
|
||||||
|
|
||||||
const distinctYears: number[] = posts
|
|
||||||
.map(post => pubDate(post).getFullYear())
|
|
||||||
.reduce<number[]>((acc, curr) => acc.includes(curr) ? acc : [...acc, curr], [])
|
|
||||||
.sort((a, b) => b - a);
|
|
||||||
|
|
||||||
function matchesYear(year: number) {
|
|
||||||
return (post: CollectionEntry<'blog'>) => pubDate(post).getFullYear() === year;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortByPubDateDescending(post1: CollectionEntry<'blog'>, post2: CollectionEntry<'blog'>) {
|
|
||||||
const date1 = pubDate(post1).getTime();
|
|
||||||
const date2 = pubDate(post2).getTime();
|
|
||||||
return date2 - date1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pubDate(post: CollectionEntry<'blog'>): Date {
|
|
||||||
if (post.data.pubDate) {
|
|
||||||
return post.data.pubDate;
|
|
||||||
}
|
|
||||||
const date = new Date(post.id);
|
|
||||||
if (isNaN(date.valueOf())) {
|
|
||||||
throw new Error(`Post ${post.id} does not have a valid publication date.`);
|
|
||||||
}
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HeadingElem = `h${headingLevel} class="p-name"`;
|
|
||||||
const SubHeadingElem = `h${headingLevel + 1}`;
|
|
||||||
const SubSubHeadingElem = `h${headingLevel + 2}`;
|
|
||||||
const AuthorElem = `p${hideAuthor ? " hidden" : ""}`;
|
|
||||||
|
|
||||||
const canonicalBlogUrl = new URL('blog', Astro.site)
|
|
||||||
---
|
|
||||||
|
|
||||||
<section class="h-feed">
|
|
||||||
<HeadingElem>
|
|
||||||
My blog
|
|
||||||
</HeadingElem>
|
|
||||||
|
|
||||||
<aside>
|
|
||||||
<AuthorElem>
|
|
||||||
This blog is written by <a class="p-author h-card" href="/">Joe Carstairs</a>
|
|
||||||
</AuthorElem>
|
|
||||||
|
|
||||||
<p hidden>
|
|
||||||
<a class="u-url" href={canonicalBlogUrl}>Permalink</a>
|
|
||||||
</p>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<slot />
|
|
||||||
|
|
||||||
{ hideSubheadings
|
|
||||||
? <ul>
|
|
||||||
{ posts.sort(sortByPubDateDescending).map(post => (
|
|
||||||
<li class="h-entry">
|
|
||||||
<SubSubHeadingElem>
|
|
||||||
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
|
|
||||||
</SubSubHeadingElem>
|
|
||||||
<p class="p-summary" set:html={post.data.description} />
|
|
||||||
<FormattedDate className="dt-published" date={pubDate(post)} />
|
|
||||||
</li>
|
|
||||||
)) }
|
|
||||||
</ul>
|
|
||||||
: distinctYears.map(year => (
|
|
||||||
<SubHeadingElem>{year}</SubHeadingElem>
|
|
||||||
<ul>
|
|
||||||
{ posts.filter(matchesYear(year)).sort(sortByPubDateDescending).map(post => (
|
|
||||||
<li class="h-entry">
|
|
||||||
<SubSubHeadingElem>
|
|
||||||
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
|
|
||||||
</SubSubHeadingElem>
|
|
||||||
<p class="p-summary" set:html={post.data.description} />
|
|
||||||
<FormattedDate className="dt-published" date={pubDate(post)} />
|
|
||||||
</li>
|
|
||||||
)) }
|
|
||||||
</ul>
|
|
||||||
)) }
|
|
||||||
|
|
||||||
{ (maxEntries !== undefined && maxEntries < allPosts.length)
|
|
||||||
? <p class="full-feed-link"><a href="/blog">All blog posts</a></p>
|
|
||||||
: <></>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
interface Props {
|
|
||||||
className?: string;
|
|
||||||
date: Date | string;
|
|
||||||
hidden?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { className, date, hidden } = Astro.props;
|
|
||||||
if (typeof(date) === 'string') {
|
|
||||||
date = new Date(date);
|
|
||||||
}
|
|
||||||
const dateStr =
|
|
||||||
date.toLocaleDateString('en-GB', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
});
|
|
||||||
---
|
|
||||||
|
|
||||||
{ hidden
|
|
||||||
? <time datetime={date.toISOString()} class={className ?? ''} hidden>{dateStr}</time>
|
|
||||||
: <time datetime={date.toISOString()} class={className ?? ''}>{dateStr}</time>
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
---
|
|
||||||
import FormattedDate from '../components/FormattedDate.astro';
|
|
||||||
import allLinks from '../data/links.ts';
|
|
||||||
|
|
||||||
type Link = (typeof allLinks)[number];
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6,
|
|
||||||
hideAuthor?: boolean,
|
|
||||||
maxEntries?: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { headingLevel = 2, hideAuthor = false, maxEntries } = Astro.props;
|
|
||||||
|
|
||||||
const headingElem = `h${headingLevel}`;
|
|
||||||
const subHeadingElem = `h${headingLevel + 1}`
|
|
||||||
|
|
||||||
const links: Link[] = maxEntries === undefined
|
|
||||||
? allLinks
|
|
||||||
: allLinks.sort(sortByDateAddedDescending).slice(0, maxEntries);
|
|
||||||
|
|
||||||
const distinctYears: string[] = links
|
|
||||||
.map(link => link.isoDateAdded.slice(0,4))
|
|
||||||
.reduce<string[]>((acc, curr) => acc.includes(curr) ? acc : [...acc, curr], [])
|
|
||||||
.sort((a, b) => Number.parseInt(b) - Number.parseInt(a));
|
|
||||||
|
|
||||||
function matchesYear(year: string) {
|
|
||||||
return (link: (typeof links)[number]) => link.isoDateAdded.slice(0,4) === year;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortByDateAddedDescending(link1: (typeof links)[number], link2: (typeof links)[number]) {
|
|
||||||
const date1 = new Date(link1.isoDateAdded).getTime();
|
|
||||||
const date2 = new Date(link2.isoDateAdded).getTime();
|
|
||||||
return date2 - date1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const canonicalLinksUrl = new URL('links', Astro.site)
|
|
||||||
---
|
|
||||||
|
|
||||||
<section class="h-feed">
|
|
||||||
<Fragment set:html={`
|
|
||||||
<${headingElem} class="p-name">
|
|
||||||
My links
|
|
||||||
</${headingElem}>
|
|
||||||
`} />
|
|
||||||
|
|
||||||
<aside>
|
|
||||||
<p hidden={hideAuthor}>
|
|
||||||
These links are collected by <a class="p-author h-card" href="/">Joe Carstairs</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p hidden>
|
|
||||||
<a class="u-url" href={canonicalLinksUrl}>Permalink</a>
|
|
||||||
</p>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{ distinctYears.map(year => (
|
|
||||||
<Fragment set:html={`
|
|
||||||
<${subHeadingElem}>
|
|
||||||
${year}
|
|
||||||
</${subHeadingElem}>
|
|
||||||
`} />
|
|
||||||
<ul>
|
|
||||||
{ links.filter(matchesYear(year)).sort(sortByDateAddedDescending).map(link => (
|
|
||||||
<li class="h-entry e-content">
|
|
||||||
<FormattedDate className="dt-published" date={link.isoDateAdded} />
|
|
||||||
<a class="u-url p-name" href={link.href} set:html={link.title} />
|
|
||||||
<p class="p-description" set:html={link.description} />
|
|
||||||
</li>
|
|
||||||
)) }
|
|
||||||
</ul>
|
|
||||||
)) }
|
|
||||||
|
|
||||||
{ (maxEntries !== undefined && maxEntries < allLinks.length)
|
|
||||||
? <p class="full-feed-link"><a href="/links">All links</a></p>
|
|
||||||
: <></>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
<section class="h-card">
|
|
||||||
<div>
|
|
||||||
<img class="u-photo" src="/images/headshot.webp" height="96" width="96" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<h1>
|
|
||||||
Joe Carstairs
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div hidden>
|
|
||||||
<a class="p-name u-url u-uid" href="https://joeac.net" rel="me">
|
|
||||||
Permalink
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="h-card__text">
|
|
||||||
<p>
|
|
||||||
Hi! 👋 My name is <span class="p-given-name">Joe</span>
|
|
||||||
<span class="p-family-name">Carstairs</span>. I’m a
|
|
||||||
<span class="p-job-title">software developer</span> at
|
|
||||||
<a class="p-org" href="https://www.scottlogic.com">Scott Logic</a>,
|
|
||||||
a Divinity student at the University of Edinburgh,
|
|
||||||
a lapsed fiddle player and a very occasional poet.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
HMU with your thoughts on Elizabethan poetry, connecting to the
|
|
||||||
Internet, and the Apocalypse of Saint John.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<small>
|
|
||||||
Or get me on
|
|
||||||
<a href="https://www.facebook.com/joe.carstairs.5" rel="me">Facebook</a>,
|
|
||||||
<a href="https://mastodon.social/@joe_carstairs" rel="me">Mastodon</a>,
|
|
||||||
<a href="https://www.linkedin.com/in/joe-carstairs-0aa936277" rel="me">LinkedIn</a>,
|
|
||||||
<a href="https://bsky.app/profile/joeacarstairs.bsky.social" rel="me">BlueSky</a>,
|
|
||||||
or <a href="https://github.com/joeacarstairs" rel="me">GitHub</a>.
|
|
||||||
</small>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
---
|
|
||||||
import type { CollectionEntry } from 'astro:content';
|
|
||||||
import { getCollection } from 'astro:content';
|
|
||||||
import FormattedDate from './FormattedDate.astro';
|
|
||||||
import { renderGemtextToHtml } from '../renderGemtextToHtml';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6,
|
|
||||||
hideAuthor?: boolean,
|
|
||||||
hideSubheadings?: boolean,
|
|
||||||
maxEntries?: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { headingLevel = 2, hideSubheadings = false, hideAuthor = false, maxEntries } = Astro.props;
|
|
||||||
|
|
||||||
const allPosts = await getCollection('microlog');
|
|
||||||
|
|
||||||
const posts = maxEntries === undefined
|
|
||||||
? allPosts
|
|
||||||
: allPosts.sort(sortByIdDescending).slice(0, maxEntries);
|
|
||||||
|
|
||||||
const distinctYears: number[] = posts
|
|
||||||
.map(post => pubDate(post).getFullYear())
|
|
||||||
.reduce<number[]>((acc, curr) => acc.includes(curr) ? acc : [...acc, curr], [])
|
|
||||||
.sort((a, b) => b - a);
|
|
||||||
|
|
||||||
function matchesYear(year: number) {
|
|
||||||
return (post: CollectionEntry<'microlog'>) => pubDate(post).getFullYear() === year;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortByIdDescending(post1: CollectionEntry<'microlog'>, post2: CollectionEntry<'microlog'>) {
|
|
||||||
return post2.id.localeCompare(post1.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pubDate(post: CollectionEntry<'microlog'>): Date {
|
|
||||||
console.warn(post.id);
|
|
||||||
const date = new Date(post.id.replace(/\.[0-9]+$/g, ''));
|
|
||||||
if (isNaN(date.valueOf())) {
|
|
||||||
throw new Error(`Post ${post.id} does not have a valid publication date.`);
|
|
||||||
}
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HeadingElem = `h${headingLevel} class="p-name"`;
|
|
||||||
const SubHeadingElem = `h${headingLevel + 1}`;
|
|
||||||
const SubSubHeadingElem = `h${headingLevel + 2}`;
|
|
||||||
const AuthorElem = `p${hideAuthor ? " hidden" : ""}`;
|
|
||||||
|
|
||||||
const canonicalMicrologUrl = new URL('microlog', Astro.site)
|
|
||||||
---
|
|
||||||
|
|
||||||
<section class="h-feed">
|
|
||||||
<HeadingElem>
|
|
||||||
My microlog
|
|
||||||
</HeadingElem>
|
|
||||||
|
|
||||||
<aside>
|
|
||||||
<AuthorElem>
|
|
||||||
This microlog is written by <a class="p-author h-card" href="/">Joe Carstairs</a>
|
|
||||||
</AuthorElem>
|
|
||||||
|
|
||||||
<p hidden>
|
|
||||||
<a class="u-url" href={canonicalMicrologUrl}>Permalink</a>
|
|
||||||
</p>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<slot />
|
|
||||||
|
|
||||||
{ hideSubheadings
|
|
||||||
? <ul>
|
|
||||||
{ posts.sort(sortByIdDescending).map(post => (
|
|
||||||
<li class="h-entry">
|
|
||||||
<SubSubHeadingElem>
|
|
||||||
<a class="u-url p-name" href={`/microlog/${post.id}`}>{post.id}</a>
|
|
||||||
</SubSubHeadingElem>
|
|
||||||
<section class="e-content" set:html={post.body} />
|
|
||||||
<FormattedDate hidden={true} className="dt-published" date={pubDate(post)} />
|
|
||||||
</li>
|
|
||||||
)) }
|
|
||||||
</ul>
|
|
||||||
: distinctYears.map(year => (
|
|
||||||
<SubHeadingElem>{year}</SubHeadingElem>
|
|
||||||
<ul>
|
|
||||||
{ posts.filter(matchesYear(year)).sort(sortByIdDescending).map(post => (
|
|
||||||
<li class="h-entry">
|
|
||||||
<SubSubHeadingElem>
|
|
||||||
<a class="u-url p-name" href={`/microlog/${post.id}`}>{post.id}</a>
|
|
||||||
</SubSubHeadingElem>
|
|
||||||
<section class="e-content" set:html={renderGemtextToHtml(post.body ?? '')} />
|
|
||||||
<FormattedDate hidden={true} className="dt-published" date={pubDate(post)} />
|
|
||||||
</li>
|
|
||||||
)) }
|
|
||||||
</ul>
|
|
||||||
)) }
|
|
||||||
|
|
||||||
{ (maxEntries !== undefined && maxEntries < allPosts.length)
|
|
||||||
? <p class="full-feed-link"><a href="/microlog">All microlog posts</a></p>
|
|
||||||
: <></>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="/">Home</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/blog">Blog</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/microlog">Microlog</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/contact">Contact</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
<script src="../scripts/otp-form-wc.ts"></script>
|
|
||||||
<link rel="stylesheet" href="/css/otp.css" />
|
|
||||||
|
|
||||||
<dialog class="otp-dialog">
|
|
||||||
<otp-form>
|
|
||||||
<form class="otp-form" class="otp-form">
|
|
||||||
<p>
|
|
||||||
I've sent a six-digit code to <span class="otp-recipient">your email address</span>.
|
|
||||||
Let me know what it is, so I can confirm this really is your email address. The
|
|
||||||
code will be valid <span class="otp-valid-until">for five minutes</span>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p hidden class="error"/>
|
|
||||||
|
|
||||||
<div class="otp-inputs">
|
|
||||||
<input maxlength="1" name="1" required autocapitalize="characters" autocomplete="one-time-code" aria-label="First digit of one-time passcode" autofocus>
|
|
||||||
<input maxlength="1" name="2" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Second digit of one-time passcode">
|
|
||||||
<input maxlength="1" name="3" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Third digit of one-time passcode">
|
|
||||||
<input maxlength="1" name="4" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Fourth digit of one-time passcode">
|
|
||||||
<input maxlength="1" name="5" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Fifth digit of one-time passcode">
|
|
||||||
<input maxlength="1" name="6" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Sixth digit of one-time passcode">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="submit" value="Verify">
|
|
||||||
<button disabled class="resend-button">Resend (60s)</button>
|
|
||||||
</form>
|
|
||||||
</otp-form>
|
|
||||||
</dialog>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export const SITE_TITLE = 'Joe Carstairs';
|
|
||||||
export const SITE_DESCRIPTION = 'Joe’s personal website.';
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { defineCollection } from "astro:content";
|
|
||||||
import { z } from "astro/zod";
|
|
||||||
|
|
||||||
import { extendedGlob } from "./loaders/extended-glob";
|
|
||||||
import { glob } from "astro/loaders";
|
|
||||||
|
|
||||||
const blog = defineCollection({
|
|
||||||
loader: extendedGlob({
|
|
||||||
pattern: "**/*.(md|mdx|gmi|html)",
|
|
||||||
base: "./src/content/blog",
|
|
||||||
postprocessSlug: (slug: string) => slug.replaceAll(/^capsule-longlog\//g, ""),
|
|
||||||
ignore: [
|
|
||||||
"capsule-longlog/index.gmi",
|
|
||||||
/* Some blog posts were ported to gemtext. Don't port them back again:
|
|
||||||
* that would lead to duplicates! The originals are likely to be better
|
|
||||||
* anyway, as MD/MDX are richer languages. */
|
|
||||||
"**/2024-01-14.gmi",
|
|
||||||
"**/2024-01-29.gmi",
|
|
||||||
"**/2024-03-30.gmi",
|
|
||||||
"**/2024-04-10.gmi",
|
|
||||||
"**/2024-04-11.gmi",
|
|
||||||
"**/2024-04-14.gmi",
|
|
||||||
"**/2024-05-02.gmi",
|
|
||||||
"**/2024-06-13.gmi",
|
|
||||||
"**/2024-07-08.gmi",
|
|
||||||
"**/2024-07-16.gmi",
|
|
||||||
"**/2024-12-17.gmi",
|
|
||||||
"**/2025-01-19.gmi",
|
|
||||||
"**/2025-01-24.gmi",
|
|
||||||
"**/2025-01-28.gmi",
|
|
||||||
"**/2025-05-02.gmi",
|
|
||||||
"**/2025-05-04.gmi",
|
|
||||||
"**/2025-06-23.gmi",
|
|
||||||
"**/2025-07-03.gmi",
|
|
||||||
"**/2025-09-18.gmi",
|
|
||||||
"**/2025-09-24.gmi",
|
|
||||||
"**/2025-10-05.gmi",
|
|
||||||
"**/2025-10-09.gmi",
|
|
||||||
"**/2025-12-11.gmi",
|
|
||||||
"**/2026-02-16.gmi",
|
|
||||||
"**/2026-03-04.gmi",
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
schema: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
hidden: z.optional(z.boolean()),
|
|
||||||
description: z.optional(z.string()),
|
|
||||||
pubDate: z.optional(z.date()),
|
|
||||||
updatedDate: z.optional(z.date()),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const microlog = defineCollection({
|
|
||||||
loader: glob({
|
|
||||||
pattern: "**/*.gmi",
|
|
||||||
base: "./src/content/microlog",
|
|
||||||
generateId: ({ entry }) => entry.replace(/\.gmi$/, ""),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const collections = { blog, microlog };
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
---
|
|
||||||
title: Harari’s Sapiens on Religion
|
|
||||||
description: >-
|
|
||||||
In which I discuss why I think Harari’s characterisation of religion
|
|
||||||
is inadequate because it’s too materialistic.
|
|
||||||
pubDate: 2024-01-14
|
|
||||||
---
|
|
||||||
|
|
||||||
I’ve been slowly re-reading Yuval Noah Harari’s 2014 classic,
|
|
||||||
<a href="https://www.ynharari.com/book/sapiens-2">Sapiens</a>,
|
|
||||||
which apart from being ridiculously over-scoped and hilariously
|
|
||||||
under-evidenced, is proving delightfully entertaining.
|
|
||||||
|
|
||||||
I’ve just finished chapter 12, covering the world history of all
|
|
||||||
religion in thirty pages. Of course, at that level of brevity,
|
|
||||||
there will be many deficiencies. But here’s some thoughts - not
|
|
||||||
terribly well organised - which stand out to me.
|
|
||||||
|
|
||||||
Hurari generally assumes a materialist metaphysic (a problem which
|
|
||||||
blights the book more generally). Nothing exists except physical stuff.
|
|
||||||
This gives him severe tunnel vision. As a consequence of this
|
|
||||||
restricting metaphysic, he is forced to adopt limiting accounts of what
|
|
||||||
the role of religion is in world history, and therefore what religion is.
|
|
||||||
|
|
||||||
> The crucial historical role of religion has been to give superhuman
|
|
||||||
> legitimacy to [all social orders and hierarchies].
|
|
||||||
> Religion can thus be defined as <em>a system of human norms and
|
|
||||||
> values that is founded on a belief in a superhuman order</em>.
|
|
||||||
> <footer>p. 234</footer>
|
|
||||||
|
|
||||||
It might seem a little unfair to criticise Harari for giving a
|
|
||||||
materialist account of religion. <i>Sapiens</i> is, after all, a
|
|
||||||
materialist world history.
|
|
||||||
|
|
||||||
But this account is just one extreme example of how that project, to
|
|
||||||
give a materialist account of world history, will inevitably lack the
|
|
||||||
metaphysical resources to really understand the human story.
|
|
||||||
|
|
||||||
On Harari’s view, any human enterprise which attempts to understand
|
|
||||||
that which transcends direct human experience is at best an effort in
|
|
||||||
imaginative story-telling. All scientific theory, theology, ethics and
|
|
||||||
metaphysics either contorted out of all recognition into a pragmatic
|
|
||||||
fiction or is cast to the flames.
|
|
||||||
|
|
||||||
In particular, it’s a view which is incapable of taking seriously some
|
|
||||||
of the most important questions human beings have grappled with in the
|
|
||||||
course of their history. Those who know me won’t be surprised at which
|
|
||||||
ones I’m going to pick out: who was the being which made their covenant
|
|
||||||
with Abraham? How is that promise being fulfilled? And who the heck was
|
|
||||||
Jesus of Nazareth?
|
|
||||||
|
|
||||||
If Harari’s characterisation of religion is adequate - and the Abrahamic
|
|
||||||
faiths come under that banner - then those questions are reduced to
|
|
||||||
nothing more profound than Doctor Who fans arguing over ‘canon’. The
|
|
||||||
question of who God is becomes a mere tool for the organisation of
|
|
||||||
society, rather than a substantial and important question on a matter
|
|
||||||
of fact.
|
|
||||||
|
|
||||||
This is a shortcoming for its own sake: a materialist account of
|
|
||||||
religion cannot adequately account for the phenomenon of religion
|
|
||||||
itself.
|
|
||||||
|
|
||||||
But it is also a shortcoming even by its own lights. Without giving
|
|
||||||
serious consideration to the substantial matter of what Harari calls
|
|
||||||
‘religion’ (which, to his mind, includes the Abrahamic faiths,
|
|
||||||
Hinduism, paganism, animism, Buddhism, Shintoism, Confucianism,
|
|
||||||
capitalism, communism and Nazism), even the material facts are
|
|
||||||
inexplicable. Why would, as Harari is keen to point out, out, people
|
|
||||||
fight and die over and over again for a fiction?
|
|
||||||
|
|
||||||
The material facts themselves prove that ‘religion’ as he construes it
|
|
||||||
is not window dressing to the real story of history. It cannot merely
|
|
||||||
serve as a mechanism in the churning of material history. It is itself
|
|
||||||
the centre of the story.
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
---
|
|
||||||
title: EUHWC Toast to the Lassies 2024
|
|
||||||
hidden: true
|
|
||||||
description: >-
|
|
||||||
At the <a href="https://www.euhwc.co.uk">EUHWC</a> Burns meet in
|
|
||||||
Ullapool last weekend, I had the last privilege of giving the Toast to
|
|
||||||
the Lassies. Particularly for the benefit of those who weren’t there,
|
|
||||||
here it is in full!
|
|
||||||
pubDate: 2024-01-29
|
|
||||||
---
|
|
||||||
|
|
||||||
Had Burns, instead of his sweet bonnie Jean,<br>
|
|
||||||
his skills poetical for to mature<br>
|
|
||||||
had any one of our club’s lassies seen<br>
|
|
||||||
he would forever have remained obscure.<br>
|
|
||||||
If he had nothing but this box of worms<br>
|
|
||||||
Scotia would have been poorer, that I’m sure.<br>
|
|
||||||
Now none of us can claim to be a Burns,<br>
|
|
||||||
I’m no poetic master, still, I’ll have a punt,<br>
|
|
||||||
though let’s be clear, I’ll do it on my terms.<br>
|
|
||||||
I’ve everywhere avoided being blunt -<br>
|
|
||||||
politeness matters more than any schema -<br>
|
|
||||||
but it is hard when Isla’s such a cunt.<br>
|
|
||||||
It was a challenge to produce a terza rima<br>
|
|
||||||
I could recite withouten snoring;<br>
|
|
||||||
you’ve been so stiff I thought youse had oedema.<br>
|
|
||||||
The bother is this year is you’ll all been boring:<br>
|
|
||||||
no drugs, no sex, no gossiping or lies,<br>
|
|
||||||
no rock and roll, and hardly any whoring.<br>
|
|
||||||
But hey well, rules is rules, I’ve had to try!<br>
|
|
||||||
At least it can’t be worse than the reply.<br>
|
|
||||||
|
|
||||||
I’ll start with Audrey, the club’s senior member,<br>
|
|
||||||
for if there’s something that I say which disconcerts her,<br>
|
|
||||||
it’s fine: the poor old girl, she won’t remember.<br>
|
|
||||||
She likes to let us think she’s a hard worker<br>
|
|
||||||
but we’re electing a third social sec…<br>
|
|
||||||
it’s pretty clear she’s just another shirker.<br>
|
|
||||||
This lady, half American, half Czech,<br>
|
|
||||||
for study, moved to Scotland for to do<br>
|
|
||||||
American history – really, what the heck?<br>
|
|
||||||
The club is so much louder thanks to you:<br>
|
|
||||||
impressive vocals for just five foot two.<br>
|
|
||||||
|
|
||||||
That woman, Willow, reggles is bespeckled<br>
|
|
||||||
with her sickle and her fishing tackle<br>
|
|
||||||
shackled by the shins while she is heckled;<br>
|
|
||||||
the way that Willow waddles maks me cackle<br>
|
|
||||||
like a speckled jackal getting tickles,<br>
|
|
||||||
worth a shekel in the tabernacle;<br>
|
|
||||||
I chuckle muckle at her love of pickles<br>
|
|
||||||
which she wiggles when she has the heart<br>
|
|
||||||
while work for the Committee’s fickle trickles.<br>
|
|
||||||
Her modus operandi: <em>you can’t rush art.</em><br>
|
|
||||||
Her reimbursements programme’s going great;<br>
|
|
||||||
any day now, she’ll maybe even start.<br>
|
|
||||||
She cannot walk without Audrey, her mate:<br>
|
|
||||||
I wonder when they’re going to consummate.<br>
|
|
||||||
|
|
||||||
Although they make them pretty tough in Peebles,<br>
|
|
||||||
the thought of actually going up a peak<br>
|
|
||||||
fills Shona Lewis with the heeble-jeebles.<br>
|
|
||||||
New car? We miss your beautiful antique!<br>
|
|
||||||
How long before this one’s also up a creek?<br>
|
|
||||||
|
|
||||||
Once there was a lass called Hannah Collier<br>
|
|
||||||
whom even hell below regarded nasty,<br>
|
|
||||||
deeply despised by all that dwells there.<br>
|
|
||||||
Dating’s proceeding slowly for our lassie;<br>
|
|
||||||
not far from giving up til she beguiles<br>
|
|
||||||
a hot Italian in Southsider: classy!<br>
|
|
||||||
At first, Michaelo seems to be all smiles<br>
|
|
||||||
till it transpires he’s one of Dante’s demons…<br>
|
|
||||||
I guess it’s back to posters of Harry Styles.<br>
|
|
||||||
One day you’ll get a decent boy, keep dreamin;<br>
|
|
||||||
somewhere there waits a handsome Mr Collier.<br>
|
|
||||||
Hopefully when she meets him she’ll no be steamin.<br>
|
|
||||||
Hannah, I’m not sure why you chose to maul your<br>
|
|
||||||
poor skeleton at Subway (she’s still tetchy)<br>
|
|
||||||
and then abandon what remains of all your<br>
|
|
||||||
dignity at Ryvoan with a Frenchie!<br>
|
|
||||||
I think he wishes that he never met ye.<br>
|
|
||||||
|
|
||||||
And has a quiet Felicia e’er been seen?<br>
|
|
||||||
The energy she has is frankly wild.<br>
|
|
||||||
I’ve never seen a hillwalker so keen!<br>
|
|
||||||
<i lang="de">Ssie ischt raschtlos und nie gelangweilt</i>.<br>
|
|
||||||
She eats raw oats with soggy protein powder:<br>
|
|
||||||
a camping pot has ne’er been worse defiled.<br>
|
|
||||||
She uses what her Maker has endowed her<br>
|
|
||||||
with: her recorder skills are off the charts;<br>
|
|
||||||
youse think I’m joking, but I wouldn’t doubt her!<br>
|
|
||||||
This lass of the land of the Rot-Gold-Schwarz<br>
|
|
||||||
will soon depart, though long we might beseech you<br>
|
|
||||||
to stay. Of course, you’ll break all of our hearts,<br>
|
|
||||||
but mine most of all. Any time, Felicia,<br>
|
|
||||||
Creag Meagaidh calls, I know routes up the rear<br>
|
|
||||||
dark and under-explored that I can teach you!<br>
|
|
||||||
I won’t deny I think it’s rather queer<br>
|
|
||||||
the things you do with chickpeas, but no matter.<br>
|
|
||||||
You’re keen, you’re quick, you’re cool, that much is clear.<br>
|
|
||||||
In fact, I think you’d make a damn good faffer:<br>
|
|
||||||
swoop down on distilleries like the Luftwaffe.<br>
|
|
||||||
|
|
||||||
And now we come to our girl Emily Topness!<br>
|
|
||||||
You’re keen for social sec. You’d suit the role<br>
|
|
||||||
because… I’m not quite sure, it’s embdy’s guess.<br>
|
|
||||||
We met your sister, and she was just as dull.<br>
|
|
||||||
No, please drone on about Icelandic soil!<br>
|
|
||||||
Poor Joe here down the front’s bored out his skull.<br>
|
|
||||||
And since I mentioned Joe – I hate to spoil<br>
|
|
||||||
it for you – but you’ve got the inferior Joe,<br>
|
|
||||||
by Jove, no joke, it’s Jock here’s got the style!<br>
|
|
||||||
Nah, write the boy a sonnet, get in the flow,<br>
|
|
||||||
Whatever you produce’ll beat by thrice<br>
|
|
||||||
your Masters thesis. What’d you got to show<br>
|
|
||||||
for months of hunting for the butterflies?<br>
|
|
||||||
‘There weren’t any.’ Oh, and have some sense,<br>
|
|
||||||
cos I’ve heard rumours – I assume they’re lies –<br>
|
|
||||||
you’ve called yourself the ‘poet in residence.’<br>
|
|
||||||
You know you can’t compete, drop the pretence.<br>
|
|
||||||
|
|
||||||
Tereza was our gear sec for last year.<br>
|
|
||||||
She helped herself to stuff: that’s factual.<br>
|
|
||||||
Now when she asks to loan a bit club gear<br>
|
|
||||||
we have to ask her to provide collateral.<br>
|
|
||||||
She picked up tin whistle pretty sharp!<br>
|
|
||||||
Which is to say, she’s not a natural.<br>
|
|
||||||
She’s nowhere happier than under tarp<br>
|
|
||||||
gazing up at the moon and stars alone<br>
|
|
||||||
somewhere distant and remote like Glen Tarff.<br>
|
|
||||||
Now what to say about Lucy Ma-the-soooon....<br>
|
|
||||||
she likes… to faff… mm hmmm… well, moving on!<br>
|
|
||||||
|
|
||||||
And now we come to Emilie the French.<br>
|
|
||||||
She seems to be nice on the trips we see her<br>
|
|
||||||
but my distrust of frogs will ne’er be quenched.<br>
|
|
||||||
Claims she’s a ‘pharmacist’? So she’s a dealer.<br>
|
|
||||||
Need some pills in a pinch? You call, she’s there<br>
|
|
||||||
at your door in her rally-approved four-wheeler.<br>
|
|
||||||
One question we have is, why are you here?<br>
|
|
||||||
Most folk are in uni, you’ve no refutin<br>
|
|
||||||
you were kicked out after second year!<br>
|
|
||||||
Now the Engineering grad, Sophia Newton.<br>
|
|
||||||
Your namesake, Isaac, was a man convicted,<br>
|
|
||||||
constructed calculus; but no computin,<br>
|
|
||||||
not even Isaac’s, could’ve e’er predicted<br>
|
|
||||||
you’d drop the Eng for creative writing!<br>
|
|
||||||
now that’s what I would call a self-inflicted<br>
|
|
||||||
inflection point! It must be quite enlightening,<br>
|
|
||||||
but that doesn’t excuse when you give us an earful.<br>
|
|
||||||
The blood boils in our veins, the rage heightening,<br>
|
|
||||||
and you’re an American, that makes me fearful.<br>
|
|
||||||
What’s your secret? You have us knackered!<br>
|
|
||||||
What are you on to always be so cheerful?<br>
|
|
||||||
Now we approach the topic of Merzbacher.<br>
|
|
||||||
Wait, she’s not here? Abandoned ship?<br>
|
|
||||||
She says she’s informatics: so she’s a hacker?<br>
|
|
||||||
She has strong views, she lets her anger rip.<br>
|
|
||||||
Poor George got an earful, full of future advice,<br>
|
|
||||||
but why hasn’t she been on another club trip?<br>
|
|
||||||
We’re cruel to focus on this list of vice;<br>
|
|
||||||
the fact remains: she’s headstrong and nice.<br>
|
|
||||||
|
|
||||||
On Skye, a lady gave her poles to Sasha,<br>
|
|
||||||
which was really nice - I mean just the best -<br>
|
|
||||||
but Sasha really didn’t have to flash her.<br>
|
|
||||||
Quick history lesson: way back, RBS<br>
|
|
||||||
led the banking system to self-destruct<br>
|
|
||||||
and left taxpayers to pick up the mess.<br>
|
|
||||||
Since then, the name’s so irredeemably fucked<br>
|
|
||||||
they’ve had to ditch the brand once and for all.<br>
|
|
||||||
There’s one lassie who I need not instruct<br>
|
|
||||||
What, these days, the Royal Bank is called<br>
|
|
||||||
cos NatWest’s nasty history of scandal<br>
|
|
||||||
didn’t stop Booth from working there at all.<br>
|
|
||||||
Nothing motivates her more than to trample<br>
|
|
||||||
upon the working class. They set her free.<br>
|
|
||||||
She sank the pound quicker than the Belgrano,<br>
|
|
||||||
because ‘there is no such thing as society,’<br>
|
|
||||||
that’s how it is, is it? All right, I see.<br>
|
|
||||||
|
|
||||||
Now, coming all the way from Glenmore Lodge,<br>
|
|
||||||
it’s Ellie’s turn! We have done what we can,<br>
|
|
||||||
although I’m scared what she’ll put in my squash.<br>
|
|
||||||
She wasn’t into Benji, but listen man,<br>
|
|
||||||
you’re lucky that you dodged her drunken benders.<br>
|
|
||||||
You’ll wake up in a tent in Kyrgystan,<br>
|
|
||||||
as for how you got there, no-one remembers,<br>
|
|
||||||
and if you’d known you’d be sleeping next to Ellie,<br>
|
|
||||||
you would’ve brought some fucking ear defenders.<br>
|
|
||||||
She’ll wrap you in bubblewrap, from your ears to your belly,<br>
|
|
||||||
cotton clothes for none, and no complaining,<br>
|
|
||||||
applying safety to the max, spare socks in your wellies.<br>
|
|
||||||
She’s always at her Mountain Leader training,<br>
|
|
||||||
practicing her night nav in the locale,<br>
|
|
||||||
pursuing QMDs - unless it’s raining.<br>
|
|
||||||
But some water should not scare our gal!<br>
|
|
||||||
She’s had much experience with the wet as of late:<br>
|
|
||||||
after all, she got on well with our navy pal.<br>
|
|
||||||
What was the age of that particular first mate?<br>
|
|
||||||
Older than your ex - always part of the plan?<br>
|
|
||||||
Ah, of course! He was a spry twenty-eight!<br>
|
|
||||||
Youth’s for the losers, let’s get you a real man,<br>
|
|
||||||
mature and rugged, but kind and astute?<br>
|
|
||||||
Just make sure he’s not as old as your gran.<br>
|
|
||||||
One request we all have is you ditch the uke:<br>
|
|
||||||
never have strings been pluckèd quite so shitely;<br>
|
|
||||||
we would all much rather be hit by a nuke.<br>
|
|
||||||
And please shut up about your nice society.<br>
|
|
||||||
We are all glad you had a fun summer,<br>
|
|
||||||
but bringing it up throws us right back to sobriety.<br>
|
|
||||||
To lose you of course would be a bummer:<br>
|
|
||||||
that is, for your carefully groomed newcomers.<br>
|
|
||||||
|
|
||||||
Now time for the main woman, El Presidente!<br>
|
|
||||||
To here, it’s been like getting stones to bleed,<br>
|
|
||||||
but in Isla Burslem’s case we’ve material aplenty!<br>
|
|
||||||
As Holy Scripture says, ‘let those who lead<br>
|
|
||||||
well be worthy of double honour,’ so<br>
|
|
||||||
your bit is double length – it’s quite the screed!<br>
|
|
||||||
I’ll start off with her brilliant boyfriend – oh!<br>
|
|
||||||
Not boyfriend! Friend? To me this rather smacks<br>
|
|
||||||
of low commitment, but what do I know?<br>
|
|
||||||
So far, he’s disappointing, but on track.<br>
|
|
||||||
What’s he up to Isla: seven minutes? neat!<br>
|
|
||||||
Despite that, he is never holding back<br>
|
|
||||||
your blossoming romance with Dr Peat.<br>
|
|
||||||
Don’t deny it, that launch was pretty hard!<br>
|
|
||||||
It’s fifth base next: that’s photos of his feet.<br>
|
|
||||||
It’s fair to say her reputation’s marred.<br>
|
|
||||||
We all regret that we did once anoint<br>
|
|
||||||
her President: her premiership’s ill-starred.<br>
|
|
||||||
Hey - you’re meant to be in charge of this joint!<br>
|
|
||||||
You’re seldom seen cos of the mountaineering<br>
|
|
||||||
meets that you’re always on. You’d made your point<br>
|
|
||||||
before you chose to go off disappearing<br>
|
|
||||||
to <em>New Zealand</em>… we get the message! Plus<br>
|
|
||||||
we’ve had enough of all your domineering:<br>
|
|
||||||
maybe it’s time we put you on a bus!<br>
|
|
||||||
Nah, I’m just joking. All I’ve said’s refutable.<br>
|
|
||||||
But the boys, we mean this next bit, all of us,<br>
|
|
||||||
so stop me Isla if this isn’t suitable<br>
|
|
||||||
but honestly we think your mum is beautiful.<br>
|
|
||||||
|
|
||||||
Alas, I have to bring an end to this rhyme.<br>
|
|
||||||
I know it wasn’t much, in our defence,<br>
|
|
||||||
the fact you used ChatGPT’s a crime.<br>
|
|
||||||
I hope I’ve not caused over much offence<br>
|
|
||||||
don’t worry, that is it, I’ve said my bit,<br>
|
|
||||||
so I’ll turn from the ladies to the gents.<br>
|
|
||||||
Yeah, don’t look away now, we wrote this shit!<br>
|
|
||||||
I see you looking at your laces, Chris!<br>
|
|
||||||
Wit without real goodwill is not legit,<br>
|
|
||||||
so boys, don’t send sincerity to piss!<br>
|
|
||||||
Why did God say he’d take our hearts of stone<br>
|
|
||||||
and give us hearts of flesh? For this, for this!<br>
|
|
||||||
Here is flesh of our flesh, bone of our bone;<br>
|
|
||||||
love, and love nothing more but God alone.<br>
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
---
|
|
||||||
title: Why Easter is the best week of the year
|
|
||||||
description: >-
|
|
||||||
Based on a talk given to my colleagues at
|
|
||||||
<a href="https://www.scottlogic.co.uk">Scott Logic</a> for Maundy
|
|
||||||
Thursday, 2024.
|
|
||||||
pubDate: 2024-03-30
|
|
||||||
---
|
|
||||||
|
|
||||||
As you might have noticed, it is Easter this week! So I'd like to take five or
|
|
||||||
five minutes of your time to share why I – and about two billion other humans
|
|
||||||
going about the place just now – think Easter is the best week of the year. And
|
|
||||||
it's got something to do with a special Christian ritual called Communion.
|
|
||||||
|
|
||||||
Communion, at its heart, is about as simple a ritual as you can get. You get
|
|
||||||
together with a bunch of other people. You share some bread, and you share some
|
|
||||||
wine.
|
|
||||||
|
|
||||||
And it’s because of this ritual that so many people regard Easter as the best
|
|
||||||
week of the year. I want to explain to you why that is, and more than that, I
|
|
||||||
want to convince you that Easter is the best week of the year for you, too!
|
|
||||||
|
|
||||||
If you’ve passed by _The Hub_ at the top of Johnstone Terrace here in
|
|
||||||
Edinburgh recently, you might have notice the banner which is draped over
|
|
||||||
the railings just now – reading, ‘RITUALS THAT UNITE US.’
|
|
||||||
|
|
||||||
Now, that might seem like an odd idea. But wouldn’t that be great, if we
|
|
||||||
actually had a ritual which could unite us? Because the world could surely do
|
|
||||||
with a bit more unity right now. The world seems so divided, and sometimes it
|
|
||||||
seems like there’s no hope for real unity.
|
|
||||||
|
|
||||||
We can see that in our politics. We’re divided about foreign policy, about
|
|
||||||
taxation policy, about trade policy, about environmental policy.
|
|
||||||
|
|
||||||
And the conflicts that we have in this country seem pretty trivial when we
|
|
||||||
remember the conflicts that are playing out in other parts of the world right
|
|
||||||
now. In Israel and Gaza. In Sudan. In Russia and Ukraine.
|
|
||||||
|
|
||||||
And there’s plenty of conflict happening on the small scale, too. Often it’s the
|
|
||||||
smallest-scale conflicts which hurt us the most deeply. Your landlord pushes you
|
|
||||||
around. That friend you trusted like no-one else in the world lets you down. The
|
|
||||||
partner or spouse you loved like no-one else in the world – you end up fighting.
|
|
||||||
|
|
||||||
It’s possible that you’re going to be reading this right now with a heavy heart
|
|
||||||
because of a broken relationship in their life. And doesn’t that hurt more than
|
|
||||||
anything else we know?
|
|
||||||
|
|
||||||
When the world is groaning so heavily under the weight of conflict, and some
|
|
||||||
banner on _The Hub_ tells us a ritual can unite us, that seems so out of
|
|
||||||
proportion to the scale of the problem, doesn’t it? What can a ritual do? A bit
|
|
||||||
of old superstition? An excuse to divide people, maybe – what can a ritual do
|
|
||||||
to unite us?
|
|
||||||
|
|
||||||
Well, two thousand years ago, a man had a meal with his friends. Together, they
|
|
||||||
shared a meal of bread and wine – which, in that time and place, was the most
|
|
||||||
ordinary meal imaginable.
|
|
||||||
|
|
||||||
And yet, in that most ordinary event imaginable, something was happening which
|
|
||||||
was totally unimaginable. As this man, Jesus, shared the elements of this meal,
|
|
||||||
he made some extraordinary statements about what he was doing: ‘take, eat, this
|
|
||||||
is my body’ – ‘drink this, all of you; this is my blood of the new covenant.’
|
|
||||||
(The word ‘covenant’ means a promise.)
|
|
||||||
|
|
||||||
He told them he wasn’t just giving them bread and wine, he was giving his body
|
|
||||||
and his blood, and a promise.
|
|
||||||
|
|
||||||
Before Jesus ate another meal, he was flogged and nailed to a cross. His blood
|
|
||||||
was spilt and his body broken, even to death.
|
|
||||||
|
|
||||||
And yet, that wasn’t the end of the Easter story. Because three days later,
|
|
||||||
mourners turned up at Jesus’ tomb to pay their respects, and found the tomb
|
|
||||||
empty, the stone rolled away. Then they became the first of crowds of
|
|
||||||
incredulous eyewitnesses to see Jesus, the same Jesus who was killed on a cross,
|
|
||||||
alive.
|
|
||||||
|
|
||||||
Some magic trick, right? But this matters a hell of a lot more than just some
|
|
||||||
magic trick. Because Jesus became the first person in history to prove that you
|
|
||||||
really can both have your cake and eat it. He gave his life, and lived! As a
|
|
||||||
result, we can have his life and our own. We can join with Jesus through the
|
|
||||||
ritual of Communion which he established, and thereby, through Jesus’ body, join
|
|
||||||
together with everyone else who takes part in that ritual, as one body. Then we
|
|
||||||
can start living our brand-new, full-fat, original-recipe life overflowing with
|
|
||||||
generosity where we too can both give our life to others and enjoy it ourselves.
|
|
||||||
Indeed, Jesus taught us and showed us that it’s precisely by giving our lives to
|
|
||||||
others that we get to truly live ourselves.
|
|
||||||
|
|
||||||
This is why, in spite of all the division which persists in the world today, two
|
|
||||||
billion people regard this week as the best week of the year. Two billion
|
|
||||||
people, from every nation on Earth, speaking thousands of languages, of every
|
|
||||||
age and culture and gender and race, who defy the divisions of this world to
|
|
||||||
insist on joining together as one body in Jesus.
|
|
||||||
|
|
||||||
Because Jesus’ new covenant, his promise to all of us, is that in an apparently
|
|
||||||
hopelessly divided world, there exists real hope for unity. And that’s why
|
|
||||||
Easter is the best week of the year.
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
title: Tracking pixels
|
|
||||||
description: Concerning a notice in the privacy policy of an HR app
|
|
||||||
pubDate: 2024-04-10
|
|
||||||
---
|
|
||||||
|
|
||||||
I recently made the mistake of reading the privacy policy of the HR app my
|
|
||||||
employer uses. Take this choice excerpt:
|
|
||||||
|
|
||||||
> **Tracking Pixels**
|
|
||||||
>
|
|
||||||
> We may place tracking pixels in our Applicant Tracking System (ATS). These
|
|
||||||
> pixels are used for analytics purposes, including to track statistical
|
|
||||||
> information around when an email sent to you is opened.
|
|
||||||
|
|
||||||
Translation:
|
|
||||||
|
|
||||||
> We reserve the right to know whether you have read our emails, how long you
|
|
||||||
> spent reading them, at what time and in what physical location you read them,
|
|
||||||
> and any other information we can get our hands on. We reserve the right to do
|
|
||||||
> this as soon as you open an email, without asking for your permission first,
|
|
||||||
> and ask for your forgiveness later. That is, if you ever find out: which you
|
|
||||||
> won't, because we also reserve the right to hide this information in a Privacy
|
|
||||||
> Policy which we know you won't read, and we design our data-harvesting monster
|
|
||||||
> to be completely undetectable to anyone except determined experts, on purpose.
|
|
||||||
> We reserve the right to leave this data lying unused in a data warehouse where
|
|
||||||
> someone might steal it, and we also reserve the right to use it, if we can
|
|
||||||
> think of something satisfactorily dastardly to do with it. On the bright side,
|
|
||||||
> we probably won't, so having your data leaked to hackers and fraudsters is
|
|
||||||
> probably the worst you have to worry about. Unless we sell your data to
|
|
||||||
> advertisers, or a ~black hole of stolen data~ Large Language Model. Oh yeah,
|
|
||||||
> we probably reserve the right to do that, too. We doubt anyone will ever check.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
title: Who consecrates the tabernacle? (Ex 29)
|
|
||||||
description: A quick, cursory and possibly completely rubbish observation I've made on Exodus 29.
|
|
||||||
pubDate: 2024-04-11
|
|
||||||
---
|
|
||||||
|
|
||||||
I've been reading Exodus recently, and the ending of Chapter 29 stuck out to me.
|
|
||||||
|
|
||||||
For pages and pages (since Chapter 25!) God has been giving Moses exact and
|
|
||||||
exorbitant instructions for how to build the tabernacle, what offerings to give
|
|
||||||
upon it, who the priests are going to be, what the priests are going to be
|
|
||||||
wearing. It's the fanciest IKEA manual you've ever read. No expense spared.
|
|
||||||
Everything is drowning in gold, silver, incense, myrrh, silk, fragrant oil, you
|
|
||||||
name it.
|
|
||||||
|
|
||||||
What's this about? The answer seems obvious: it's to sanctify the temple! To
|
|
||||||
make it beautiful enough and pure enough that even God could live there.
|
|
||||||
|
|
||||||
But right at the end of Chapter 29, in vv 43-44, there's this wonderful twist:
|
|
||||||
|
|
||||||
> ... and the place will be consecrated by **my glory**. So **I** will
|
|
||||||
> consecrate the tent of meeting and the altar and will consecrate Aaron and his
|
|
||||||
> sons to serve me as priests. (NIV, emphasis mine)
|
|
||||||
|
|
||||||
All this work, and at the end of the day, it's **God** who sanctifies his own
|
|
||||||
temple? So why all the fuss? Well, it looks like the final two verses of the
|
|
||||||
chapter give us the answer:
|
|
||||||
|
|
||||||
> Then I will dwell among the Israelites and be their God. They will know that I
|
|
||||||
> am the <span class="small-caps">Lord</span> their God, who brought them out of
|
|
||||||
> Egypt so that I might dwell among them. I am the
|
|
||||||
> <span class="small-caps">Lord</span> their God.
|
|
||||||
|
|
||||||
So on a cursory reading, it looks like God is telling them something
|
|
||||||
extraordinary about the kind of God he is. He is not like the other gods, who
|
|
||||||
demand this kind of worship to appease their pride. Nor is there, unlike the
|
|
||||||
other gods, anything the Israelites could do to make a place habitable for him.
|
|
||||||
That's because there's nothing the Israelites could do to cleanse themselves of
|
|
||||||
their sin: only the 'glory' of God could do that. And indeed, God has no need of
|
|
||||||
worship to puff up his pride, since his glory existed before the world was made,
|
|
||||||
and human beings can do nothing to either add to it or subtract from it.
|
|
||||||
|
|
||||||
Instead of a vain god who seeks tribute, this is a Father God who seeks to
|
|
||||||
'dwell' (literally to 'camp') among his people. The God who has put into motion
|
|
||||||
a rescue plan to pull the Israelites out of Egypt specifically for this purpose.
|
|
||||||
As it was in the beginning, when God dwelt in the Garden with Adam and Eve. This
|
|
||||||
is his mission. This is what he's like. And he wants his people to know this, so
|
|
||||||
that they can dwell together.
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
---
|
|
||||||
title: God Is Not Great, initial thoughts
|
|
||||||
description: >-
|
|
||||||
My spark notes on Hitchen's classic 2007 polemic against religion, plus some
|
|
||||||
initial thoughts on how I want to respond to it.
|
|
||||||
pubDate: 2024-04-14
|
|
||||||
---
|
|
||||||
|
|
||||||
These are my 'spark notes' on _God Is Not Great_, Christopher Hitchen's classic
|
|
||||||
2007 polemic against religion in all its forms, and call to adopt secular
|
|
||||||
humanism as its rightful replacement.
|
|
||||||
|
|
||||||
The book can be coherently read as a collection of independent essays. That
|
|
||||||
said, with a few exceptions, each chapter in _God Is Not Great_ contributes to
|
|
||||||
one of three main themes, and I think this is a helpful way of summarising the
|
|
||||||
overall movement of the book. The three themes I've identified are as follows:
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<a href="#1-religion-is-evil">Religion is evil</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#2-religion-is-untrue">Religion is untrue</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#3-secular-humanism-is-a-better-alternative-to-religion">Secular humanism is a better alternative to religion</a>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
As for the exceptions: Chapters 13 and 17 in part contribute to both the first
|
|
||||||
theme and the third, while Chapters 1, 3, 12 and 14 don't fit into these broad
|
|
||||||
themes, and are self-standing.
|
|
||||||
|
|
||||||
## 1. Religion is evil
|
|
||||||
|
|
||||||
- Chp 2
|
|
||||||
- Religion is violent, because:
|
|
||||||
- It has to be missionary, because it is insecure in its own beliefs (p17)
|
|
||||||
- It provokes tribalistic conflict, in a similar manner to racism (pp35-36)
|
|
||||||
- Chp 4
|
|
||||||
- Religion is bad for your health, because:
|
|
||||||
- Faith in medicine is a threat to religion's thrall (p47)
|
|
||||||
- Religious doctrines may contradict sound medical advice
|
|
||||||
- The right to religious freedom may be abused to deflect criticism of
|
|
||||||
unhealthy practices (p50)
|
|
||||||
- Religion has a special relationship with child abuse, and is incapable of
|
|
||||||
accepting open moral criticism of itself for this
|
|
||||||
- Chp 13
|
|
||||||
- Religion tends towards evil, because it requires fanaticism in order to
|
|
||||||
spread, and fanaticism tends towards evil (p192)
|
|
||||||
- Chp 16
|
|
||||||
- Religion causes child abuse, because:
|
|
||||||
- Terrifying children with eschatology is child abuse
|
|
||||||
- Religious education is propaganda and should not be inflicted on children
|
|
||||||
who are not yet mature enough to respond to it rationally
|
|
||||||
- Religion consistently mandates cruel genital mutilation of children
|
|
||||||
- Christians and Muslims have spread misinformation about masturbation,
|
|
||||||
which leads to dangerously sexually repressed adult men, which in turn
|
|
||||||
leads to sexual abuse of children by those men (as well as sexual abuse of
|
|
||||||
women)
|
|
||||||
- Child abuse in churches is not a case of a few bad eggs, it is
|
|
||||||
institutional and based on an ideological need to control the minds and
|
|
||||||
sexual organs of children
|
|
||||||
- Religion institutionalised torture in medieval Europe
|
|
||||||
- Religion makes honest and nuanced debate about abortion impossible, because:
|
|
||||||
- Nuanced debate is pushed out by extreme and implausible religious doctrine
|
|
||||||
- Religious people would rather use the unborn as objects of doctrine than
|
|
||||||
human beings in need of protection
|
|
||||||
- Chp 17
|
|
||||||
- Religion is the only reason anti-Semitism is possible
|
|
||||||
- Religion in its fullest expression is indistinguishable from
|
|
||||||
totalitarianism, because:
|
|
||||||
- The defining characteristic of both religion and totalitarianism is the
|
|
||||||
absolute right to rule of the ruler, even when they rule with caprice
|
|
||||||
- Religion and totalitarianism are also characterised by the need to
|
|
||||||
extinguish heresy with violence
|
|
||||||
- Religion and totalitarianism alike must propose a total solution to all
|
|
||||||
life's problems, require blind faith from its adherents, and demand all
|
|
||||||
aspects of life public and private be submitted to total supervision. This
|
|
||||||
doesn't bring out the best in us
|
|
||||||
- Religious/totalitarian systems are unable to take accountability and
|
|
||||||
therefore improve over time, in contrast to secular humanist systems
|
|
||||||
- History has proven this as fact:
|
|
||||||
- In the ancient world, religious totalitarianism was the normal form of
|
|
||||||
government
|
|
||||||
- Calvin, the inspiration for the Presbyterian Christian tradition, which
|
|
||||||
included South African apartheid, was the epitome of a totalitarian
|
|
||||||
dictator, demanding total control on the private lives of his citizens
|
|
||||||
in Geneva, on the pain of humiliation in this life and eternal torment
|
|
||||||
in the next
|
|
||||||
- History has also proven that, rather than standing in opposition to the
|
|
||||||
supposedly secular totalitarianisms of the twentieth century, religion
|
|
||||||
actually aided and abetted totalitarianism:
|
|
||||||
- Rome supported fascist movements throughout Europe, including Italy,
|
|
||||||
Hungary, Spain, and Ireland
|
|
||||||
- Rome accommodated Naziism by handing over control of its schools,
|
|
||||||
permitting the use of parish records to identify those with Jewish
|
|
||||||
ancestry, disbanding Catholic opposition political parties, declaring
|
|
||||||
Hitler's birthday a Church holiday, and running the 'rat line' to South
|
|
||||||
America after the military defeat of Naziism
|
|
||||||
- Although not quite as arse-licking as the Vatican, Germany's Protestant
|
|
||||||
churches also mostly capitulated to Nazi totalitarianism
|
|
||||||
- Japanese soldiers committed enormous atrocities across the Far East in
|
|
||||||
the name of their god-emperor, Hirohito
|
|
||||||
- The strategy of the Communists was first, to use religion as a prop to
|
|
||||||
gain power, and then to replace religion with itself. Notice the
|
|
||||||
striking commonalities between religion and communist totalitarianism:
|
|
||||||
- Infallible leaders
|
|
||||||
- A permanent war on heresy
|
|
||||||
- Institutionalised torture
|
|
||||||
- Scapegoating the innocent rather than accepting accountability for
|
|
||||||
failures
|
|
||||||
- Justifying any means necessary in order to achieve an ultimate end
|
|
||||||
|
|
||||||
## 2. Religion is untrue
|
|
||||||
|
|
||||||
- Chp 5
|
|
||||||
- Religion was a barbaric attempt to explain physical phenomena. Science now
|
|
||||||
does a much better job, so religion can be discarded as a redundant theory
|
|
||||||
- Chp 6
|
|
||||||
- Religion is 'solipsistic', which is to say:
|
|
||||||
- Religion divides the world into an in-group and an out-group
|
|
||||||
- When the in-group receives good fortune, that is interpreted as God's
|
|
||||||
blessing
|
|
||||||
- When the in-group receives bad fortune, this is inexplicable
|
|
||||||
- Whatever happens to the out-group is irrelevant
|
|
||||||
- This is a redundant theory which explains little, and therefore we should
|
|
||||||
not believe it
|
|
||||||
- Chp 7
|
|
||||||
- We should believe that the Pentateuch is a fiction, because:
|
|
||||||
- Exodus is inconsistent with the archaeological evidence
|
|
||||||
- Textual evidence in Deuteronomy suggests the texts were written much later
|
|
||||||
than the supposed events were supposed to have taken place
|
|
||||||
- Throughout the Pentateuch, Moses is referred to in the third person, which
|
|
||||||
is not consistent with the claim that Moses himself authored it
|
|
||||||
- The Pentateuch contains events we should _hope_ to be false, such as Moses
|
|
||||||
ordering multiple massacres, and the Ten Commandments classifying wives as
|
|
||||||
their husbands' property
|
|
||||||
- The Pentateuch - indeed, the whole Bible - is limited in scope to a small
|
|
||||||
corner of the Middle East, which is not consistent with the universal
|
|
||||||
nature of the supposed God who is supposed to have inspired its authorship
|
|
||||||
- Chp 8
|
|
||||||
- We should not believe the Gospels, because:
|
|
||||||
- Matthew and Luke disagree on the virgin conception
|
|
||||||
- Matthew and Luke disagree on the genealogy of Jesus
|
|
||||||
- Matthew and Luke disagree on when Mary and Jesus escaped to Egypt
|
|
||||||
- Luke dates the birth of Christ during both the reign of Herod in Judea
|
|
||||||
and the governorship of Quirinius in Syria, but these two events did not
|
|
||||||
overlap, so this is impossible
|
|
||||||
- As far as we know, the Romans did not, and would not, demand that the
|
|
||||||
people assemble in one place in order to be counted for a census
|
|
||||||
- The Gospels disagree about the Sermon on the Mount
|
|
||||||
- The Gospels disagree about the Anointing of Jesus
|
|
||||||
- The Gospels disagree about the Crucifixion and Resurrection of Jesus
|
|
||||||
- The Gospels disagree about the betrayal of Judas
|
|
||||||
- The Gospels disagree about the denial of Peter
|
|
||||||
- John suggests he thinks Jesus was probably born in Galilee to an
|
|
||||||
ordinary family with no proven link to King David's genealogy
|
|
||||||
- Mary's attested behaviour during the ministry of Jesus is not consistent
|
|
||||||
with the Nativity story
|
|
||||||
- John 8:3-11 (the stoning of the woman caught in adultery) is a later
|
|
||||||
insertion to John's Gospel
|
|
||||||
- Chp 9: the Koran is borrowed from a hotchpotch of Jewish and Christian
|
|
||||||
myths
|
|
||||||
- Chp 10
|
|
||||||
- Miracles (such as the Resurrection of Jesus) should not be believed,
|
|
||||||
because:
|
|
||||||
- See Hume's _On Miracles_
|
|
||||||
- Extraordinary claims require extraordinary evidence
|
|
||||||
- Believing miracles is comparable to believing reports of aliens (p144)
|
|
||||||
- Chp 11
|
|
||||||
- Religions are founded in credulity
|
|
||||||
- Chp 15
|
|
||||||
- The crucifixion story makes no sense, because:
|
|
||||||
- I'm supposed to be morally responsible for Adam's sin, but I amn't!
|
|
||||||
- Even Adam can't be fully blamed for Adam's sin, because he was set up!
|
|
||||||
- The Jews who crucified Jesus are supposed to be blameworthy for their
|
|
||||||
crime, even though the crucifixion was allegedly both necessary and
|
|
||||||
inevitable
|
|
||||||
- I'm allegedly given the free will to either accept or deny the offer,
|
|
||||||
even though denying the offer will lead to an eternity of torment: any
|
|
||||||
sensible God who cared a mite would not have given me the choice
|
|
||||||
- Religious rules are impossible to follow, and this leads to either
|
|
||||||
spiritual policing, organised hypocrisy, or both
|
|
||||||
|
|
||||||
## 3. Secular humanism is a better alternative to religion
|
|
||||||
|
|
||||||
- Chp 13
|
|
||||||
- Faith has inspired great heroism. But the heroism is better explained by the
|
|
||||||
heroes' humanism than it is by their faith itself
|
|
||||||
- For example, Martin Luther King Junior didn't really preach Christianity,
|
|
||||||
because he preached forgiveness, while Christ preached eternal torment for
|
|
||||||
the 'inattentive' (pp175-6)
|
|
||||||
- Chp 17
|
|
||||||
- Religion is the only thing sustaining anti-Semitism
|
|
||||||
- Secular humanist political systems can take accountability, respond to
|
|
||||||
criticism and improve over time, while religious systems exempt themselves
|
|
||||||
from legitimate criticism, stifling progress
|
|
||||||
- The only alternative to totalitarianism is pluralism, which is inherently
|
|
||||||
secular
|
|
||||||
- Chp 18
|
|
||||||
- Secular humanism has been a powerful positive force throughout history in
|
|
||||||
face of oppression by religion, as proved by example:
|
|
||||||
- Socrates proved that conscience is innate, and that a great way to mock
|
|
||||||
dogma is with satire which pretends to accept that dogma
|
|
||||||
- Lucretius, Democritus and Epicurus had better explanations for the way the
|
|
||||||
world worked than religion (which is why their work was suppressed in
|
|
||||||
Christian Europe and nearly lost forever as a result). Once rediscovered,
|
|
||||||
their ideas kick-started the Scientific Revolution in Europe
|
|
||||||
- Spinoza's deistic ideas had a huge influence despite Jews collaborating
|
|
||||||
with their Christian oppressors to try and ban his writing out of
|
|
||||||
existence
|
|
||||||
- Boyle and Voltaire may have been closet atheists, agnostics or deists
|
|
||||||
- Kant 'overthrew' the cosmological and ontological arguments for the
|
|
||||||
existence of God, and proved (by means of the Categorical Imperative) that
|
|
||||||
human decency does not require any theological assumptions
|
|
||||||
- Let's chuck some more names in the ring: Gibbon, Hume, Paine, Franklin,
|
|
||||||
Darwin, Einstein
|
|
||||||
- Jews were once doubly ghettoed: on the outside by oppressive Christians,
|
|
||||||
and on the inside by oppressive self-racialisation. Secular humanism freed
|
|
||||||
Jews from both these ghettoes, which in turn led to an outpouring of
|
|
||||||
secular Jewish brilliance
|
|
||||||
- Ancient Jews were on the road to a quasi-secular Hellenism, before that
|
|
||||||
was ruined forever by the tyrannical and fanatical Judas Maccabeus, with
|
|
||||||
disastrous consequences for the history of Western civilisation. The
|
|
||||||
Abrahamic religions we know today were not inevitable, and it is possible
|
|
||||||
to imagine what Western history would have been like without it
|
|
||||||
- Chp 19
|
|
||||||
- Secular humanism is a better alternative to religion, because:
|
|
||||||
- Religion requires clinging to immovable dogma and being unwilling to
|
|
||||||
change your mind
|
|
||||||
- Seeking truth requires being willing to change your mind
|
|
||||||
- Secular humanism is therefore on the side of seeking truth, and religion
|
|
||||||
is on the side of wilful ignorance
|
|
||||||
- Secular humanism is on the side of progress, because it is what enables the
|
|
||||||
expansion of scientific knowledge and the development of new technology
|
|
||||||
|
|
||||||
## The odds and ends
|
|
||||||
|
|
||||||
- Chp 1: an introduction to the themes of the book with little substantial
|
|
||||||
- Chp 3: titled 'A Short Digression on the Pig', it does what it says on the tin
|
|
||||||
- Chp 12: titled 'A Coda: How Religions End', it does what it says on the tin
|
|
||||||
- Chp 14: contrary to the hopes of some Westerners disillusioned with organised
|
|
||||||
Western religion, Eastern religions like Buddhism, Hinduism and Taoism are not
|
|
||||||
a solution to the problems of religion
|
|
||||||
|
|
||||||
I would be interested to try writing apologetic and/or evangelistic responses to
|
|
||||||
these ideas. But it's not a priority for me right now. Anyway, if I ever want to
|
|
||||||
come back to it, I'll probably come back to these spark notes to give me a head
|
|
||||||
start.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
---
|
|
||||||
title: How I made YouTube work for me
|
|
||||||
description: >-
|
|
||||||
I just learned YouTube channels have an RSS feed. This is terrific news.
|
|
||||||
pubDate: 2024-05-02
|
|
||||||
---
|
|
||||||
|
|
||||||
One of my bad habits in life is wandering through YouTube. I've always had
|
|
||||||
AutoPlay turned off, but I still found myself switching off and sleepwalking
|
|
||||||
from one 'recommendation' to the next.
|
|
||||||
|
|
||||||
A lot of what I watch in these times is crap. It's often when I'm tired and just
|
|
||||||
want to switch off, so naturally enough, longer, more emotionally or
|
|
||||||
intellectually material – in other words, exactly the kind of videos I actually
|
|
||||||
**do** want to watch – I ignore. (For me, it's often mediocre sketch comedy
|
|
||||||
videos. That's my poison, it turns out.)
|
|
||||||
|
|
||||||
Now, there is a time for switching off and doing something light. But I don't
|
|
||||||
want watching junk videos to be my answer. Any more than when I'm hungry after
|
|
||||||
a long day of work, I don't want to be resorting to junk food all the time. I
|
|
||||||
really want to get out of this bad habit, and though it's not a huge problem,
|
|
||||||
I've still not been able to fully kick it, either.
|
|
||||||
|
|
||||||
Still, there are YouTube channels I really _want_ to keep up with. Musicians.
|
|
||||||
Short film channels. Video essayists. And, yes, even one or two sketch channels:
|
|
||||||
though YouTube is a bit flooded with sketch comedy, some it is really good and
|
|
||||||
brings me joy, and that's a good thing!
|
|
||||||
|
|
||||||
This is where RSS comes in.
|
|
||||||
|
|
||||||
So if you don't know, an RSS file is a file someone puts on their website which
|
|
||||||
tells you, in a standard format, what pages are on their website. It's a pretty
|
|
||||||
old standard in Web terms, and it's very stable. There are a wide range of apps
|
|
||||||
out there, called 'feed readers', which you can use to keep track of RSS feeds,
|
|
||||||
notifying you when something new gets published in your website and putting it
|
|
||||||
in a feed for you to review.
|
|
||||||
|
|
||||||
It turns out YouTube channels have RSS feeds.
|
|
||||||
|
|
||||||
This is terrific news.
|
|
||||||
|
|
||||||
It means I can use my RSS feed reader to subscribe to YouTube channels, instead
|
|
||||||
of YouTube's own subscription system.
|
|
||||||
|
|
||||||
And that means I don't need any of YouTube's personalisation features. I can
|
|
||||||
delete all my search history, all my watch history, and stop them from
|
|
||||||
collecting any more. This means YouTube is now incapable of providing
|
|
||||||
recommendations that I'm actually likely to click on. Which is exactly what I
|
|
||||||
want.
|
|
||||||
|
|
||||||
This is what my YouTube homepage looks like right now:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
So I can both subscribe to the channels I'm genuinely interested in, and not
|
|
||||||
get sucked into watching junk.
|
|
||||||
|
|
||||||
Awesome!
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
---
|
|
||||||
title: LLMs do not understand anything
|
|
||||||
description: >-
|
|
||||||
Save this for the next time someone tells you that LLMs 'understand' things.
|
|
||||||
pubDate: 2024-06-13
|
|
||||||
---
|
|
||||||
|
|
||||||
LLMs do not understand what they are talking about. They just don't. It's not
|
|
||||||
how they're built. They have a statistical model of language, not a semantic
|
|
||||||
one. Philosophical puzzles about whether silicon can be conscious
|
|
||||||
_do not arise_, because LLMs are _not even close to the right shape_ for having
|
|
||||||
_anything like_ 'understanding'.
|
|
||||||
|
|
||||||
If you don't believe me, there are plenty of examples out there on the Internet,
|
|
||||||
but this is as good as any. It includes Chat GPT 4o explaining in detail why
|
|
||||||
\~\~A → \~A (which is a classical contradiction) is trivially true in classical
|
|
||||||
logic.
|
|
||||||
|
|
||||||
It's even better given that I had explicitly asked it to explain why that
|
|
||||||
sentence implies the trivial logic, not why it is trivially true. And even had
|
|
||||||
the explanation not been complete garbage from beginning to end, it would only
|
|
||||||
have shown that the sentence was _true_, not that it was trivial.
|
|
||||||
|
|
||||||
In other words, the output:
|
|
||||||
|
|
||||||
- Attempts to prove a contradiction (unprompted!)
|
|
||||||
- Confuses the concepts 'truth' and 'triviality'
|
|
||||||
- Is irrelevant to the prompt
|
|
||||||
|
|
||||||
In case you want it handy, I'll put the full conversation down below. The good
|
|
||||||
bit is at the end. The only edits I've made are to replace TeX syntax with
|
|
||||||
Unicode.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<dl>
|
|
||||||
<dt>
|
|
||||||
Me
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
You have the following axioms. <br>
|
|
||||||
1. |- (A -> B) -> A <br>
|
|
||||||
2. |- (~B -> ~A) -> (A -> B) <br>
|
|
||||||
3. |- (A -> (B -> C)) -> ((A -> B) -> (A -> C)) <br>
|
|
||||||
4. A -> B, A |- B <br>
|
|
||||||
Prove ~~A -> A. <br>
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
<dt>
|
|
||||||
Chat GPT 4o
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
To prove ~~ A → A, we will use the given axioms and inference rules. Let's denote the axioms as follows:
|
|
||||||
|
|
||||||
1. ⊢ (A → B) → A
|
|
||||||
2. ⊢ (\~ B → \~ A) → (A → B)
|
|
||||||
3. ⊢ (A → (B → C)) → ((A → B) → (A → C))
|
|
||||||
4. If A → B and A, then ⊢ B
|
|
||||||
|
|
||||||
Here is the proof step-by-step:
|
|
||||||
|
|
||||||
1. **Axiom (2) Instantiation:**
|
|
||||||
From axiom 2, we have:
|
|
||||||
|
|
||||||
⊢ (\~ A → \~\~ A) → (\~\~ A → A)
|
|
||||||
|
|
||||||
Let B = A and A = \~ A.
|
|
||||||
|
|
||||||
2. **Intermediate Step:**
|
|
||||||
To use this axiom, we need to show \~ A → \~\~ A.
|
|
||||||
|
|
||||||
3. **Negation Introduction:**
|
|
||||||
By definition of negation, \~ A → \~\~ A is a tautology, because if A is false, then \~ A is true, and \~\~ A (which is A) must be false, making \~ A → \~\~ A always true.
|
|
||||||
|
|
||||||
4. **Modus Ponens Application:**
|
|
||||||
Since \~ A → \~\~ A is true, we apply modus ponens (axiom 4) to derive:
|
|
||||||
|
|
||||||
⊢ \~\~ A → A
|
|
||||||
|
|
||||||
|
|
||||||
Thus, we have proved \~\~ A → A using the given axioms and inference rules.
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
---
|
|
||||||
title: Doctor Who, Gayness, and the Church
|
|
||||||
description: >-
|
|
||||||
Series 14 of Doctor Who has a schizophrenic relationship with Christianity.
|
|
||||||
It’s also gay. I think there might be a connection.
|
|
||||||
pubDate: 2024-07-08
|
|
||||||
---
|
|
||||||
|
|
||||||
I’ve recently finished the most recent series of Doctor Who, series fourteen (or
|
|
||||||
‘Season One’ as our new benevolent overlords at Disney+ are styling it.) It’s
|
|
||||||
pretty fun, by the way. I can recommend it for light watching[^1].
|
|
||||||
|
|
||||||
There’s a few discussion points coming out of that series that would be worth
|
|
||||||
dwelling on. But I’ve been particularly thinking about the schizophrenic
|
|
||||||
attitude the series has taken to Christianity, along with how gay this series
|
|
||||||
is: and what these two themes might fit together, to give us something important
|
|
||||||
to say about Jesus Christ, being gay, and the universe.
|
|
||||||
|
|
||||||
## The Doctor’s schizophrenic relationship with Christianity
|
|
||||||
|
|
||||||
First, let’s look at that schizophrenic attitude to Christianity I mentioned.
|
|
||||||
|
|
||||||
The series ticks up an impressive tally of explicit or strongly implicit
|
|
||||||
references to Christian beliefs and morals, and **none** of them are positive.
|
|
||||||
|
|
||||||
In episode 3, _Boom_, the far-future Anglican church has become an army,
|
|
||||||
with the ranks of the clergy becoming equivalent to arms-bearing ranks in the
|
|
||||||
soldiery. The Doctor claims that ‘the Church’ has been an army for most of
|
|
||||||
its history[^2], and that his companion Ruby Sunday (and therefore us) has
|
|
||||||
been living in a ‘blip’. An all-powerful arms company has tricked the Church
|
|
||||||
into fighting a non-existent foe in order to keep them buying weapons. The
|
|
||||||
Doctor attributes their ability to fall for such a deception to religious faith:
|
|
||||||
|
|
||||||
> I mean, most armies would notice that they were fighting smoke and shadows,
|
|
||||||
> but not this lot, Ruby, you know why? They have faith. Faith! The magic word
|
|
||||||
> that keeps you never having to think for yourself.
|
|
||||||
|
|
||||||
In episode 4, _73 Yards_, Kate Lethbridge-Stewart, head of UNIT, makes this
|
|
||||||
offhand comment, suggesting the oft-repeated claim that all religion is founded
|
|
||||||
in credulity[^3]:
|
|
||||||
|
|
||||||
> That’s what we do, all of us. We see something inexplicable, and invent the
|
|
||||||
> rules to make it work. Mankind saw the sun rise and created God: or we saw the
|
|
||||||
> arrival of a Sontaran, one or the other.
|
|
||||||
|
|
||||||
In episode 5, _Dot and Bubble_, The Doctor is rejected by the people he is
|
|
||||||
trying to save. They rant that he is ‘not one of us’, call his claims about the
|
|
||||||
TARDIS ‘magic’ and ‘voodoo’, and in the same breath assert that it is their
|
|
||||||
‘God-given duty to maintain the standards of Finetime’ (their space colony).
|
|
||||||
|
|
||||||
In episode 6, _Rogue_, a party of murderous aliens turns up at an upper-class
|
|
||||||
Georgian dance party and demand to be married. The priest denies them, not on
|
|
||||||
the grounds that they are unrepentant murderers with obviously no intention
|
|
||||||
of taking their marriage vows seriously, but on the grounds that they are
|
|
||||||
‘creatures from hell’, _ie_ ‘you don’t look like us’[^4].
|
|
||||||
|
|
||||||
There are other references to Christianity here and there which are, in
|
|
||||||
themselves, neutral or ambiguous. This adds to the sense that Christianity is an
|
|
||||||
important theme for the series.
|
|
||||||
|
|
||||||
But the message is clear. It’s consistently drilled in: Christian beliefs and
|
|
||||||
morals are stupid and bigoted.
|
|
||||||
|
|
||||||
And yet, when the series culminates in its epic two-part finale, the story is
|
|
||||||
plastered wall-to-wall with Christ-like imagery. It transpires that the Doctor
|
|
||||||
has unwittingly been acting as an ‘Angel of Death’ by carrying the invisible God
|
|
||||||
of Death, Sutekh, on his TARDIS, infecting everyone he goes near with the curse
|
|
||||||
of death. Finally, through one of his children, the curse is unleashed, and
|
|
||||||
everyone in the universe is given over to death. Even in death, they continue to
|
|
||||||
suffer, and one of the characters describes themselves as being in ‘hell’. The
|
|
||||||
Doctor battles Sutekh face-to-face and defeats him. Sutekh becomes one of the
|
|
||||||
only creatures which The Doctor, usually a staunch pacifist, can bring himself
|
|
||||||
to kill. Then, the whole world is restored to life. Ruby Sunday even gets to
|
|
||||||
meet her birth mother for the first time, and their relationship is wonderfully
|
|
||||||
restored.
|
|
||||||
|
|
||||||
So on the one hand, any outward sign of Christianity is despised, but the heart
|
|
||||||
of the Christian story — the Resurrection — becomes the template for the
|
|
||||||
climactic redemption story which ties the whole series together.
|
|
||||||
|
|
||||||
That’s what I mean when I say this series has a ‘schizophrenic’ relationship
|
|
||||||
to Christianity.
|
|
||||||
|
|
||||||
## What’s this got to do with gayness?
|
|
||||||
|
|
||||||
In series fourteen, Doctor Who goes gay.
|
|
||||||
|
|
||||||
I’m not playing culture wars here. The Doctor literally kisses a man in episode
|
|
||||||
six.
|
|
||||||
|
|
||||||
But it’s not just that. When we first meet his companion, Ruby Sunday (in the
|
|
||||||
Christmas special), she’s busy falling in love with a woman. Ncuti Gatwa plays
|
|
||||||
The Doctor camp (brilliantly by the way). The Doctor refers to Ruby as ‘babes’.
|
|
||||||
It’s got ‘gay’ written all over it, and this is definitely on purpose.
|
|
||||||
|
|
||||||
<figure>
|
|
||||||
<img
|
|
||||||
alt="An official promotional picture of Ncuti Gatwa looking lustily into the camera"
|
|
||||||
src="/images/blog/2024/06/30/ncuti-gatwa-promo-pic.webp"
|
|
||||||
/>
|
|
||||||
<figcaption>
|
|
||||||
Look at this official BBC promo pic and tell me Ncuti Gatwa isn’t playing
|
|
||||||
The Doctor gay.
|
|
||||||
</figcaption>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
From this perspective, it’s not hard to imagine where the hostility to the
|
|
||||||
Church might come from.
|
|
||||||
|
|
||||||
The Church has failed to teach well and Biblically on sexuality, at least in
|
|
||||||
the last couple of centuries. Everyone has suffered as a result of this, but gay
|
|
||||||
people often feel the sharpness of this particularly keenly[^5].
|
|
||||||
|
|
||||||
The Church has put its weight behind a variety of unhelpful teachings on
|
|
||||||
sexuality over the centuries[^6]. But two, contradictory, ones stand out as
|
|
||||||
particularly salient today.
|
|
||||||
|
|
||||||
One is **prudishness**. Sex is evil. Sex is the origin of all evil: the Devil
|
|
||||||
seduced Eve, and Eve seduced Adam, and that’s when it all went downhill. Sex
|
|
||||||
is naughty. Sex is bad. Remember when Jesus said, ‘whoever looks at a woman to
|
|
||||||
lust for her has already committed adultery with her in his heart’ (Matt 5:28)?
|
|
||||||
He was saying that **all sexual desire** is sinful. Remember when Paul advised,
|
|
||||||
‘It is good for a man not to touch a woman’ (1 Cor 7:1)? He was saying that it’s
|
|
||||||
**bad** for a man to touch a woman **under any circumstances**.
|
|
||||||
|
|
||||||
This is, of course, false and dreadful teaching. The truth is that sex is a gift
|
|
||||||
from God, given so that we can bear his image by loving each other in this most
|
|
||||||
intimate and wonderful way. This is the consistent message of both Testaments.
|
|
||||||
But the idea that there’s somehow something **inherently wrong** with sex has
|
|
||||||
undeniably been a part of the Church’s teaching since at least the Victorian
|
|
||||||
period (and possibly a great deal longer than that).
|
|
||||||
|
|
||||||
The other is that sex is an **essential aspect of humanity**. If you aren’t
|
|
||||||
having sex, you’re missing out on an irreplacable part of your created purpose.
|
|
||||||
This has reacted explosively with the ideas of the Sexual Revolution. The
|
|
||||||
net result is that we have not so much been **freed to have sex** as we’ve been
|
|
||||||
**enslaved to have sex**. Virginity is an embarassment – both for men and for
|
|
||||||
women.
|
|
||||||
|
|
||||||
The radical Biblical idea that you can have a completely fulfilled life, deeply
|
|
||||||
enriched by loving relationships, **without** having sex or getting married,
|
|
||||||
is forgotten.
|
|
||||||
|
|
||||||
Everyone has been harmed by these teachings. But those who experience
|
|
||||||
significant attraction towards the same sex have been harmed double.
|
|
||||||
|
|
||||||
Faced by the impossible demands of bad Church teaching on sexuality on top
|
|
||||||
of the complexities of living with same-sex desire has left those people with
|
|
||||||
nowhere to go. They can _just say ‘no’_, and be made to feel that they’re
|
|
||||||
missing out on completing their full humanity. Or they can indulge their
|
|
||||||
same-sex attraction, and be judged not only promiscuous but a pervert to boot.
|
|
||||||
|
|
||||||
No wonder so many gay people have given up on the Church (and I haven’t even
|
|
||||||
talked about discrimination or violence towards gay people).
|
|
||||||
|
|
||||||
And no wonder that The Doctor has become strikingly anti-Christian at the same
|
|
||||||
time as it has become strikingly gay.
|
|
||||||
|
|
||||||
## What next?
|
|
||||||
|
|
||||||
Doctor Who’s criticisms of the Church hurt when they hit the mark. They hurt
|
|
||||||
more when they’re unfair. But that’s not the point. That point is this: the
|
|
||||||
Church has lost control of the conversation. And we’ve lost control of the
|
|
||||||
conversation because we threw it away. We threw it away encased in bomb-proof
|
|
||||||
concrete and left it to sink to the bottom of the deepest available ocean
|
|
||||||
trench.
|
|
||||||
|
|
||||||
If we in the Church are feeling hurt, we should start by feeling hurt by
|
|
||||||
our own sin.
|
|
||||||
|
|
||||||
The work of regaining trust on the question of sexuality will be the work of
|
|
||||||
decades: and that’s if we start working full-pelt right now. But there is hope.
|
|
||||||
|
|
||||||
I was encouraged last December by the visit to my local church, Bruntsfield
|
|
||||||
Evangelical, of _Living Out_[^7], a charity dedicated to helping churches across
|
|
||||||
the UK talk about sexuality. Ed and Andy, both same-sex attracted men, led us
|
|
||||||
through talks and discussions, and played recordings of perspectives from their
|
|
||||||
same-sex attracted female colleagues who couldn’t make it on the day.
|
|
||||||
|
|
||||||
They were primarily there to talk to us about how to support people like them:
|
|
||||||
same-sex attracted Christians who might be in our church who believe they are
|
|
||||||
called to singleness, helping them to thrive, living their true selves openly,
|
|
||||||
surrounded by love and empowered to share their love with others, just as we
|
|
||||||
want for everyone in our church. In fact, I heard some of the best news I’d ever
|
|
||||||
heard about my own sexuality, even though I am in the minority of people who
|
|
||||||
have never experienced significant same-sex attraction.
|
|
||||||
|
|
||||||
Whether or not you agree with their stance that the right place for sex is
|
|
||||||
within a marriage between a man and a woman, charities like _Living Out_ are
|
|
||||||
driving the Church in the right direction: serious, Biblical sexual doctrine
|
|
||||||
which helps us to realise in practice how we are all made in the image of the
|
|
||||||
God of love.
|
|
||||||
|
|
||||||
And how needed! How desperately needed! And Doctor Who gives us a little glimpse
|
|
||||||
of that, too.
|
|
||||||
|
|
||||||
Because not only is the series apparently anti-Christian, it also expresses a
|
|
||||||
need for Christian salvation. The Resurrection story is one which everyone needs
|
|
||||||
to hear, to have their death transformed to life, to have their full humanity
|
|
||||||
affirmed and celebrated and tended and to delight in it and to see it flourish.
|
|
||||||
To know the God of love and life, behind all and over all, with a plan and a
|
|
||||||
means to defeat the grip of death on the world.
|
|
||||||
|
|
||||||
So let’s keep having those frank conversations about sexuality in the Church.
|
|
||||||
It matters for all of us.
|
|
||||||
|
|
||||||
[^1]: Unless you’re bothered by
|
|
||||||
[plotholes big enough to fly a TARDIS through](https://www.doctorwhotv.co.uk/doctor-who-opinion-empire-death-2-102716.htm).
|
|
||||||
Oh, and you can probably skip episodes 1 and 3.
|
|
||||||
|
|
||||||
[^2]: For the record, this is flatly false. Though exceptions are widespread, the
|
|
||||||
overwhelming mainstream opinion is that priests should not bear arms. This is
|
|
||||||
not new, but has been the consistent orthodoxy, taken straight from Jesus’
|
|
||||||
teachings by the Church Fathers and maintained constantly since then. Can the
|
|
||||||
Church be legitimately criticised for its use of violence? Absolutely, let’s
|
|
||||||
have that discussion. Has the Church almost always been a **literal army**? No.
|
|
||||||
The Church has **never** been an army.
|
|
||||||
|
|
||||||
[^3]: See _God is Not Great_ by Christopher Hitchens, Chapter 11 for an epitome
|
|
||||||
example of this put forward by a prominent atheist apologist.
|
|
||||||
|
|
||||||
[^4]: In the spirit of charity, I have to accept that this admits of multiple
|
|
||||||
interpretations. It is possible that by calling them ‘creatures from hell’,
|
|
||||||
the priest is referring precisely to their unrepentant murderousness. However,
|
|
||||||
the priest has already tried to avoid the creature’s gaze by the pitiably
|
|
||||||
schoolboyish ploy of **looking elsewhere**, priming the viewers to think
|
|
||||||
that the priest is a silly coward: certainly not the kind to make a noble,
|
|
||||||
principled defence of justice with his head in the jaws of death. Plus, if
|
|
||||||
we interpret ‘creatures from hell’ as meaning ‘you don’t look like us so you
|
|
||||||
must be evil’, that would fit with the consistent framing of the series: that
|
|
||||||
religious morality is equivalent to bigotry.
|
|
||||||
|
|
||||||
[^5]: Though it must be remembered that many gay people have remained and
|
|
||||||
thrived within the Church in spite of the challenges, serving in diverse ways,
|
|
||||||
not least the priesthood.
|
|
||||||
|
|
||||||
[^6]: I found the Ezra Institute to give a good
|
|
||||||
[introduction to the history of Church teachings on sexuality](https://www.ezrainstitute.com/resource-library/articles/sex-and-the-history-of-christianity).
|
|
||||||
|
|
||||||
[^7]: For a great and humane introduction to what these guys are like, have a
|
|
||||||
listen to this [Living Out podcast episode](https://www.livingout.org/resources/podcasts/68/misstep-7-godliness-is-heterosexuality-the-plausibility-problem-7).
|
|
||||||
I get that if you haven’t grappled with issues like these before, it can be
|
|
||||||
really challenging, and you might find it hard to trust people who take a
|
|
||||||
different view to yourself. I know: I’ve been there myself! The best way to
|
|
||||||
start is to listen to a human voice.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user