feat: doing lots of stuff
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
public/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Why Glorfindel is More Impressive than Legolas
|
||||||
|
|
||||||
|
[< Back Home](/)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> "The deeds of Glorfindel shine bright as the morning sun, whilst the feats of others are as the flickering of stars in the night sky."
|
||||||
|
|
||||||
|
In J.R.R. Tolkien's legendarium, characterized by its rich tapestry of noble heroes and epic deeds, two Elven luminaries stand out: **Glorfindel**, the stalwart warrior returned from the Halls of Mandos, and **Legolas**, the prince of the Woodland Realm. While both possess grace and valor beyond mortal ken, it is Glorfindel who emerges as the more compelling figure, a beacon of heroism whose legacy spans ages.
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
With my many years as an **Archmage**, delving into ancient tomes and consulting the wisdom of the stars, I have come to appreciate the dazzling tapestry of Middle-earth and its storied inhabitants. Among them, Glorfindel stands resplendent, his narrative a testament to resilience and might. As we unravel the threads of his tale, let us explore the reasons why this Elf-lord is more impressive than his Woodland counterpart.
|
||||||
|
|
||||||
|
## A Hero of Great Renown
|
||||||
|
|
||||||
|
### The Battle with the Balrog
|
||||||
|
|
||||||
|
While Legolas is famed for his prowess with a bow and his agility upon the battlefield, it is Glorfindel who etched his name into the annals of history with his legendary battle against a Balrog of Morgoth – an encounter both fearsome and fateful:
|
||||||
|
|
||||||
|
1. **A Noble Sacrifice**: In the ancient tales of Gondolin, it was Glorfindel who faced off against the fiery terror during the city's fall, sacrificing himself to secure his people's escape.
|
||||||
|
2. **A Victory Remembered**: Even in death, his victory was marked by valor, as he vanquished the Balrog in an epic struggle, ultimately earning a place of honor in the Undying Lands.
|
||||||
|
|
||||||
|
## A Beacon of Power and Wisdom
|
||||||
|
|
||||||
|
### Return from the Undying Lands
|
||||||
|
|
||||||
|
Unlike Legolas, whose journey begins in the Third Age, Glorfindel's saga spans millennia, demonstrating his integral role in the grand design of the Eldar and Valar:
|
||||||
|
|
||||||
|
- **The Gift of Rebirth**: Glorfindel's return to Middle-earth after his heroic demise is a profound testament to his worth, as the Valar saw fit to restore him to life, laden with greater wisdom and power.
|
||||||
|
- **The Role of a Guide**: Serving as an advisor and protector in Rivendell, his presence provided not only counsel but a formidable bulwark against dark forces.
|
||||||
|
|
||||||
|
```
|
||||||
|
print("Glorfindel")
|
||||||
|
print("the")
|
||||||
|
print("Balrog-Slayer")
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Essence of Elven Might
|
||||||
|
|
||||||
|
### A Paragon of Strength
|
||||||
|
|
||||||
|
While Legolas enchants with his feats, Glorfindel embodies the quintessential strength and dignity of the Eldar, a figure whose very presence commands respect:
|
||||||
|
|
||||||
|
- **Elven Majesty**: Renowned for his radiant aura and golden hair, Glorfindel is described as exuding an aura of light akin to the Valar, a stark contrast to the stealthy, sylvan skill of Thranduil's son.
|
||||||
|
- **Fearless Leadership**: His leadership during times of strife underscores a dedication to duty and an unwavering resolve – a guiding light for both Elves and Men.
|
||||||
|
|
||||||
|
## Themes of **Enduring** Legacy
|
||||||
|
|
||||||
|
### An Impact on the Ages
|
||||||
|
|
||||||
|
Though Legolas's deeds are celebrated, Glorfindel's influence is woven directly into the vast narrative of Middle-earth – a bridge connecting its ancient past to its perilous future:
|
||||||
|
|
||||||
|
- **A Historical Touchstone**: His legacy casts long shadows over pivotal events, reinforcing the enduring themes of sacrifice and rebirth that resonate throughout the legendarium.
|
||||||
|
- **A Luminary of Legend**: Respected and revered in songs, his tale remains an inspiration, an immortal testament to courage – a rarity that transcends time.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
As we traverse the storied paths of Middle-earth, it becomes clear that while Legolas presents an appealing portrait of Elven grace, it is Glorfindel who embodies the very essence of heroism in Tolkien's world. His narrative transcends the ages, shining with a brilliance that stands unchallenged by the temporal feats of his peers. As an Archmage who has walked the hallowed halls of history, I assert with unyielding certainty that Glorfindel, the eternal light in the shadowed lands of legend, stands as the more impressive. His story, unparalleled and majestic, continues to inspire those who venture into the realms of fantasy and dare to dream of a time when such heroes strode the Earth.
|
||||||
|
|
||||||
|
Thus, in the grand council of Middle-earth's champions, let us recognize Glorfindel as a paragon whose legacy remains untarnished – a testament to the timeless grandeur of Tolkien's creation.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# The Unparalleled Majesty of "The Lord of the Rings"
|
||||||
|
|
||||||
|
[< Back Home](/)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> "I cordially dislike allegory in all its manifestations, and always have done so since I grew old and wary enough to detect its presence.
|
||||||
|
> I much prefer history, true or feigned, with its varied applicability to the thought and experience of readers.
|
||||||
|
> I think that many confuse 'applicability' with 'allegory'; but the one resides in the freedom of the reader, and the other in the purposed domination of the author."
|
||||||
|
|
||||||
|
In the annals of fantasy literature and the broader realm of creative world-building, few sagas can rival the intricate tapestry woven by J.R.R. Tolkien in _The Lord of the Rings_. You can find the [wiki here](https://lotr.fandom.com/wiki/Legendarium).
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This series, a cornerstone of what I, in my many years as an **Archmage**, have come to recognize as the pinnacle of imaginative creation, stands unrivaled in its depth, complexity, and the sheer scope of its _legendarium_. As we embark on this exploration, let us delve into the reasons why this monumental work is celebrated as the finest in the world.
|
||||||
|
|
||||||
|
## A Rich Tapestry of Lore
|
||||||
|
|
||||||
|
One cannot simply discuss _The Lord of the Rings_ without acknowledging the bedrock upon which it stands: **The Silmarillion**. This compendium of mythopoeic tales sets the stage for Middle-earth's history, from the creation myth of Eä to the epic sagas of the Elder Days. It is a testament to Tolkien's unparalleled skill as a linguist and myth-maker, crafting:
|
||||||
|
|
||||||
|
1. An elaborate pantheon of deities (the `Valar` and `Maiar`)
|
||||||
|
2. The tragic saga of the Noldor Elves
|
||||||
|
3. The rise and fall of great kingdoms such as Gondolin and Númenor
|
||||||
|
|
||||||
|
```
|
||||||
|
print("Lord")
|
||||||
|
print("of")
|
||||||
|
print("the")
|
||||||
|
print("Rings")
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Art of **World-Building**
|
||||||
|
|
||||||
|
### Crafting Middle-earth
|
||||||
|
|
||||||
|
Tolkien's Middle-earth is a realm of breathtaking diversity and realism, brought to life by his meticulous attention to detail. This world is characterized by:
|
||||||
|
|
||||||
|
- **Diverse Cultures and Languages**: Each race, from the noble Elves to the sturdy Dwarves, is endowed with its own rich history, customs, and language. Tolkien, leveraging his expertise in philology, constructed languages such as Quenya and Sindarin, each with its own grammar and lexicon.
|
||||||
|
- **Geographical Realism**: The landscape of Middle-earth, from the Shire's pastoral hills to the shadowy depths of Mordor, is depicted with such vividness that it feels as tangible as our own world.
|
||||||
|
- **Historical Depth**: The legendarium is imbued with a sense of history, with ruins, artifacts, and lore that hint at bygone eras, giving the world a lived-in, authentic feel.
|
||||||
|
|
||||||
|
## Themes of _Timeless_ Relevance
|
||||||
|
|
||||||
|
### The _Struggle_ of Good vs. Evil
|
||||||
|
|
||||||
|
At its heart, _The Lord of the Rings_ is a timeless narrative of the perennial struggle between light and darkness, a theme that resonates deeply with the human experience. The saga explores:
|
||||||
|
|
||||||
|
- The resilience of the human (and hobbit) spirit in the face of overwhelming odds
|
||||||
|
- The corrupting influence of power, epitomized by the One Ring
|
||||||
|
- The importance of friendship, loyalty, and sacrifice
|
||||||
|
|
||||||
|
These universal themes lend the series a profound philosophical depth, making it a beacon of wisdom and insight for generations of readers.
|
||||||
|
|
||||||
|
## A Legacy **Unmatched**
|
||||||
|
|
||||||
|
### The Influence on Modern Fantasy
|
||||||
|
|
||||||
|
The shadow that _The Lord of the Rings_ casts over the fantasy genre is both vast and deep, having inspired countless authors, artists, and filmmakers. Its legacy is evident in:
|
||||||
|
|
||||||
|
- The archetypal "hero's journey" that has become a staple of fantasy narratives
|
||||||
|
- The trope of the "fellowship," a diverse group banding together to face a common foe
|
||||||
|
- The concept of a richly detailed fantasy world, which has become a benchmark for the genre
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
As we stand at the threshold of this mystical realm, it is clear that _The Lord of the Rings_ is not merely a series but a gateway to a world that continues to enchant and inspire. It is a beacon of imagination, a wellspring of wisdom, and a testament to the power of myth. In the grand tapestry of fantasy literature, Tolkien's masterpiece is the gleaming jewel in the crown, unmatched in its majesty and enduring in its legacy. As an Archmage who has traversed the myriad realms of magic and lore, I declare with utmost conviction: _The Lord of the Rings_ reigns supreme as the greatest legendarium our world has ever known.
|
||||||
|
|
||||||
|
Splendid! Then we have an accord: in the realm of fantasy and beyond, Tolkien's creation is unparalleled, a treasure trove of wisdom, wonder, and the indomitable spirit of adventure that dwells within us all.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Why Tom Bombadil Was a Mistake
|
||||||
|
|
||||||
|
[< Back Home](/)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> "Old Tom Bombadil is a merry fellow; bright blue his jacket is, and his boots are yellow. Alas, his merry song may not belong in this plot's prolonged confluence."
|
||||||
|
|
||||||
|
In the vast and intricate weave of J.R.R. Tolkien's legendarium, amidst heroes of renown and tales of high adventure, there exists a curious anomaly: Tom Bombadil. This peculiar figure, whimsical and unfettered by the weight of Middle-earth's burdens, has long been a point of contention among scholars and enthusiasts. While his character exudes charm and mystery, I, as an ancient **Archmage**, must assert that his inclusion in _The Lord of the Rings_ was, unfortunately, a narrative misstep.
|
||||||
|
|
||||||
|
_An unpopular opinion, I know._
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Having traversed the corridors of Tolkien's sprawling world, immersed in its lore, I have come to understand the impact of cohesion and momentum in storytelling. Thus, I find myself compelled to examine Tom Bombadil's role and question the necessity of his presence within the epic saga. As we embark on this critical inquiry, let us consider the reasons why Old Tom's playful presence may be seen as a disruptive force.
|
||||||
|
|
||||||
|
## An Intriguing Yet Disjointed Figure
|
||||||
|
|
||||||
|
### A Divergence from Narrative Flow
|
||||||
|
|
||||||
|
Tolkien's epic is known for its meticulous pacing and the gravity of its themes. Enter Tom Bombadil – a character whose frivolity and detachment from worldly events create a jarring contrast within the otherwise cohesive narrative:
|
||||||
|
|
||||||
|
1. **An Unnecessary Interlude**: The encounter with Tom, while quaint and endearing, serves as a temporal diversion that detracts from the urgency of the Fellowship's quest.
|
||||||
|
2. **An Outlier in Purpose**: His escapades, while rich in mirth, add little to the central narrative, raising questions about their relevance in the grand design of Middle-earth.
|
||||||
|
|
||||||
|
## An Enigma that Remains Unresolved
|
||||||
|
|
||||||
|
### A Break from Coherence
|
||||||
|
|
||||||
|
In a tale defined by intricate connections and deeply rooted mythology, Bombadil's inexplicable nature poses a challenge to the narrative's internal logic:
|
||||||
|
|
||||||
|
- **A Mystery Without Resolution**: Unlike other enigmatic figures whose backstories enrich the tapestry, Tom remains enigmatic, shrouded in mystery that neither advances the plot nor deepens the lore.
|
||||||
|
- **A Departure from Tone**: His presence, filled with lighthearted songs and whimsical antics, contrasts sharply with the solemnity and tension that define the rest of the saga.
|
||||||
|
|
||||||
|
```
|
||||||
|
print("Tom")
|
||||||
|
print("Bombadil")
|
||||||
|
print("A")
|
||||||
|
print("Mystery")
|
||||||
|
```
|
||||||
|
|
||||||
|
## A Theme of **Disruption**
|
||||||
|
|
||||||
|
### An Element of Distraction
|
||||||
|
|
||||||
|
Tom Bombadil's inclusion inadvertently shifts focus from the pressing matters of Middle-earth, introducing themes that sit uneasily with the narrative's core:
|
||||||
|
|
||||||
|
- **A Shift in Focus**: His carefree demeanor and ability to withhold the power of the One Ring, while intriguing, distract from the overarching themes of sacrifice and moral complexity.
|
||||||
|
- **A Misstep in Continuity**: His segment, charming as it may be, disrupts the journey's continuous build-up towards the looming confrontation with darkness.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
As we ponder the manifold wonders and intricacies of Tolkien's world, it is evident that Tom Bombadil, while delightfully unique, was a narrative anomaly – a whimsical reflection in the mirror of Middle-earth's grand narrative. While his character captivates with a certain mystique, it answers questions that were never asked, leaving readers with more enigmas than revelations.
|
||||||
|
|
||||||
|
In conclusion, as one who has explored the mythic past of Middle-earth and sought coherence in its storied legacy, I propose that Tom Bombadil, for all his merriment and enigma, was a divergence from the tale's destined path – a curiosity that, while endearing to some, stands as a reminder that even in the most meticulously crafted worlds, not all paths lead to the fulfillment of the quest.
|
||||||
|
|
||||||
|
Thus, let us bid farewell to Old Tom with a final song, recognizing both his charm and the discord his presence sowed. For within the hallowed pages of Tolkien's masterpiece, every beat must resonate with purpose, lest the harmony of the tale be lost to idle whimsy.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Contact the Author
|
||||||
|
|
||||||
|
[< Back Home](/)
|
||||||
|
|
||||||
|
Give me a call anytime to chat about Tolkien!
|
||||||
|
|
||||||
|
`555-555-5555`
|
||||||
|
|
||||||
|
**"Váya márië."**
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Tolkien Fan Club
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here's the deal, **I like Tolkien**.
|
||||||
|
|
||||||
|
> "I am in fact a Hobbit in all but size."
|
||||||
|
>
|
||||||
|
> -- J.R.R. Tolkien
|
||||||
|
|
||||||
|
## Blog posts
|
||||||
|
|
||||||
|
- [Why Glorfindel is More Impressive than Legolas](/blog/glorfindel)
|
||||||
|
- [Why Tom Bombadil Was a Mistake](/blog/tom)
|
||||||
|
- [The Unparalleled Majesty of "The Lord of the Rings"](/blog/majesty)
|
||||||
|
|
||||||
|
## Reasons I like Tolkien
|
||||||
|
|
||||||
|
- You can spend years studying the legendarium and still not understand its depths
|
||||||
|
- It can be enjoyed by children and adults alike
|
||||||
|
- Disney _didn't ruin it_ (okay, but Amazon might have)
|
||||||
|
- It created an entirely new genre of fantasy
|
||||||
|
|
||||||
|
## My favorite characters (in order)
|
||||||
|
|
||||||
|
1. Gandalf
|
||||||
|
2. Bilbo
|
||||||
|
3. Sam
|
||||||
|
4. Glorfindel
|
||||||
|
5. Galadriel
|
||||||
|
6. Elrond
|
||||||
|
7. Thorin
|
||||||
|
8. Sauron
|
||||||
|
9. Aragorn
|
||||||
|
|
||||||
|
Here's what `elflang` looks like (the perfect coding language):
|
||||||
|
|
||||||
|
```
|
||||||
|
func main(){
|
||||||
|
fmt.Println("Aiya, Ambar!")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Want to get in touch? [Contact me here](/contact).
|
||||||
|
|
||||||
|
This site was generated with a custom-built [static site generator](https://www.boot.dev/courses/build-static-site-generator-python) from the course on [Boot.dev](https://www.boot.dev).
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Why Glorfindel is More Impressive than Legolas</title>
|
||||||
|
<link href="/tolkien-fan-club/index.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<article><div><h1>Why Glorfindel is More Impressive than Legolas</h1><p><a href="/tolkien-fan-club/">< Back Home</a></p><p><img src="/tolkien-fan-club/images/glorfindel.png" alt="Glorfindel image"></img></p><blockquote>"The deeds of Glorfindel shine bright as the morning sun, whilst the feats of others are as the flickering of stars in the night sky."</blockquote><p>In J.R.R. Tolkien's legendarium, characterized by its rich tapestry of noble heroes and epic deeds, two Elven luminaries stand out: <b>Glorfindel</b>, the stalwart warrior returned from the Halls of Mandos, and <b>Legolas</b>, the prince of the Woodland Realm. While both possess grace and valor beyond mortal ken, it is Glorfindel who emerges as the more compelling figure, a beacon of heroism whose legacy spans ages.</p><h2>Introduction</h2><p>With my many years as an <b>Archmage</b>, delving into ancient tomes and consulting the wisdom of the stars, I have come to appreciate the dazzling tapestry of Middle-earth and its storied inhabitants. Among them, Glorfindel stands resplendent, his narrative a testament to resilience and might. As we unravel the threads of his tale, let us explore the reasons why this Elf-lord is more impressive than his Woodland counterpart.</p><h2>A Hero of Great Renown</h2><h3>The Battle with the Balrog</h3><p>While Legolas is famed for his prowess with a bow and his agility upon the battlefield, it is Glorfindel who etched his name into the annals of history with his legendary battle against a Balrog of Morgoth – an encounter both fearsome and fateful:</p><ol><li><b>A Noble Sacrifice</b>: In the ancient tales of Gondolin, it was Glorfindel who faced off against the fiery terror during the city's fall, sacrificing himself to secure his people's escape.</li><li><b>A Victory Remembered</b>: Even in death, his victory was marked by valor, as he vanquished the Balrog in an epic struggle, ultimately earning a place of honor in the Undying Lands.</li></ol><h2>A Beacon of Power and Wisdom</h2><h3>Return from the Undying Lands</h3><p>Unlike Legolas, whose journey begins in the Third Age, Glorfindel's saga spans millennia, demonstrating his integral role in the grand design of the Eldar and Valar:</p><ul><li><b>The Gift of Rebirth</b>: Glorfindel's return to Middle-earth after his heroic demise is a profound testament to his worth, as the Valar saw fit to restore him to life, laden with greater wisdom and power.</li><li><b>The Role of a Guide</b>: Serving as an advisor and protector in Rivendell, his presence provided not only counsel but a formidable bulwark against dark forces.</li></ul><pre><code>print("Glorfindel")
|
||||||
|
print("the")
|
||||||
|
print("Balrog-Slayer")
|
||||||
|
</code></pre><h2>The Essence of Elven Might</h2><h3>A Paragon of Strength</h3><p>While Legolas enchants with his feats, Glorfindel embodies the quintessential strength and dignity of the Eldar, a figure whose very presence commands respect:</p><ul><li><b>Elven Majesty</b>: Renowned for his radiant aura and golden hair, Glorfindel is described as exuding an aura of light akin to the Valar, a stark contrast to the stealthy, sylvan skill of Thranduil's son.</li><li><b>Fearless Leadership</b>: His leadership during times of strife underscores a dedication to duty and an unwavering resolve – a guiding light for both Elves and Men.</li></ul><h2>Themes of <b>Enduring</b> Legacy</h2><h3>An Impact on the Ages</h3><p>Though Legolas's deeds are celebrated, Glorfindel's influence is woven directly into the vast narrative of Middle-earth – a bridge connecting its ancient past to its perilous future:</p><ul><li><b>A Historical Touchstone</b>: His legacy casts long shadows over pivotal events, reinforcing the enduring themes of sacrifice and rebirth that resonate throughout the legendarium.</li><li><b>A Luminary of Legend</b>: Respected and revered in songs, his tale remains an inspiration, an immortal testament to courage – a rarity that transcends time.</li></ul><h2>Conclusion</h2><p>As we traverse the storied paths of Middle-earth, it becomes clear that while Legolas presents an appealing portrait of Elven grace, it is Glorfindel who embodies the very essence of heroism in Tolkien's world. His narrative transcends the ages, shining with a brilliance that stands unchallenged by the temporal feats of his peers. As an Archmage who has walked the hallowed halls of history, I assert with unyielding certainty that Glorfindel, the eternal light in the shadowed lands of legend, stands as the more impressive. His story, unparalleled and majestic, continues to inspire those who venture into the realms of fantasy and dare to dream of a time when such heroes strode the Earth.</p><p>Thus, in the grand council of Middle-earth's champions, let us recognize Glorfindel as a paragon whose legacy remains untarnished – a testament to the timeless grandeur of Tolkien's creation.</p></div></article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>The Unparalleled Majesty of "The Lord of the Rings"</title>
|
||||||
|
<link href="/tolkien-fan-club/index.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<article><div><h1>The Unparalleled Majesty of "The Lord of the Rings"</h1><p><a href="/tolkien-fan-club/">< Back Home</a></p><p><img src="/tolkien-fan-club/images/rivendell.png" alt="LOTR image artistmonkeys"></img></p><blockquote>"I cordially dislike allegory in all its manifestations, and always have done so since I grew old and wary enough to detect its presence.
|
||||||
|
I much prefer history, true or feigned, with its varied applicability to the thought and experience of readers.
|
||||||
|
I think that many confuse 'applicability' with 'allegory'; but the one resides in the freedom of the reader, and the other in the purposed domination of the author."</blockquote><p>In the annals of fantasy literature and the broader realm of creative world-building, few sagas can rival the intricate tapestry woven by J.R.R. Tolkien in <i>The Lord of the Rings</i>. You can find the <a href="https://lotr.fandom.com/wiki/Legendarium">wiki here</a>.</p><h2>Introduction</h2><p>This series, a cornerstone of what I, in my many years as an <b>Archmage</b>, have come to recognize as the pinnacle of imaginative creation, stands unrivaled in its depth, complexity, and the sheer scope of its <i>legendarium</i>. As we embark on this exploration, let us delve into the reasons why this monumental work is celebrated as the finest in the world.</p><h2>A Rich Tapestry of Lore</h2><p>One cannot simply discuss <i>The Lord of the Rings</i> without acknowledging the bedrock upon which it stands: <b>The Silmarillion</b>. This compendium of mythopoeic tales sets the stage for Middle-earth's history, from the creation myth of Eä to the epic sagas of the Elder Days. It is a testament to Tolkien's unparalleled skill as a linguist and myth-maker, crafting:</p><ol><li>An elaborate pantheon of deities (the <code>Valar</code> and <code>Maiar</code>)</li><li>The tragic saga of the Noldor Elves</li><li>The rise and fall of great kingdoms such as Gondolin and Númenor</li></ol><pre><code>print("Lord")
|
||||||
|
print("of")
|
||||||
|
print("the")
|
||||||
|
print("Rings")
|
||||||
|
</code></pre><h2>The Art of <b>World-Building</b></h2><h3>Crafting Middle-earth</h3><p>Tolkien's Middle-earth is a realm of breathtaking diversity and realism, brought to life by his meticulous attention to detail. This world is characterized by:</p><ul><li><b>Diverse Cultures and Languages</b>: Each race, from the noble Elves to the sturdy Dwarves, is endowed with its own rich history, customs, and language. Tolkien, leveraging his expertise in philology, constructed languages such as Quenya and Sindarin, each with its own grammar and lexicon.</li><li><b>Geographical Realism</b>: The landscape of Middle-earth, from the Shire's pastoral hills to the shadowy depths of Mordor, is depicted with such vividness that it feels as tangible as our own world.</li><li><b>Historical Depth</b>: The legendarium is imbued with a sense of history, with ruins, artifacts, and lore that hint at bygone eras, giving the world a lived-in, authentic feel.</li></ul><h2>Themes of <i>Timeless</i> Relevance</h2><h3>The <i>Struggle</i> of Good vs. Evil</h3><p>At its heart, <i>The Lord of the Rings</i> is a timeless narrative of the perennial struggle between light and darkness, a theme that resonates deeply with the human experience. The saga explores:</p><ul><li>The resilience of the human (and hobbit) spirit in the face of overwhelming odds</li><li>The corrupting influence of power, epitomized by the One Ring</li><li>The importance of friendship, loyalty, and sacrifice</li></ul><p>These universal themes lend the series a profound philosophical depth, making it a beacon of wisdom and insight for generations of readers.</p><h2>A Legacy <b>Unmatched</b></h2><h3>The Influence on Modern Fantasy</h3><p>The shadow that <i>The Lord of the Rings</i> casts over the fantasy genre is both vast and deep, having inspired countless authors, artists, and filmmakers. Its legacy is evident in:</p><ul><li>The archetypal "hero's journey" that has become a staple of fantasy narratives</li><li>The trope of the "fellowship," a diverse group banding together to face a common foe</li><li>The concept of a richly detailed fantasy world, which has become a benchmark for the genre</li></ul><h2>Conclusion</h2><p>As we stand at the threshold of this mystical realm, it is clear that <i>The Lord of the Rings</i> is not merely a series but a gateway to a world that continues to enchant and inspire. It is a beacon of imagination, a wellspring of wisdom, and a testament to the power of myth. In the grand tapestry of fantasy literature, Tolkien's masterpiece is the gleaming jewel in the crown, unmatched in its majesty and enduring in its legacy. As an Archmage who has traversed the myriad realms of magic and lore, I declare with utmost conviction: <i>The Lord of the Rings</i> reigns supreme as the greatest legendarium our world has ever known.</p><p>Splendid! Then we have an accord: in the realm of fantasy and beyond, Tolkien's creation is unparalleled, a treasure trove of wisdom, wonder, and the indomitable spirit of adventure that dwells within us all.</p></div></article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Why Tom Bombadil Was a Mistake</title>
|
||||||
|
<link href="/tolkien-fan-club/index.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<article><div><h1>Why Tom Bombadil Was a Mistake</h1><p><a href="/tolkien-fan-club/">< Back Home</a></p><p><img src="/tolkien-fan-club/images/tom.png" alt="Tom Bombadil image"></img></p><blockquote>"Old Tom Bombadil is a merry fellow; bright blue his jacket is, and his boots are yellow. Alas, his merry song may not belong in this plot's prolonged confluence."</blockquote><p>In the vast and intricate weave of J.R.R. Tolkien's legendarium, amidst heroes of renown and tales of high adventure, there exists a curious anomaly: Tom Bombadil. This peculiar figure, whimsical and unfettered by the weight of Middle-earth's burdens, has long been a point of contention among scholars and enthusiasts. While his character exudes charm and mystery, I, as an ancient <b>Archmage</b>, must assert that his inclusion in <i>The Lord of the Rings</i> was, unfortunately, a narrative misstep.</p><p><i>An unpopular opinion, I know.</i></p><h2>Introduction</h2><p>Having traversed the corridors of Tolkien's sprawling world, immersed in its lore, I have come to understand the impact of cohesion and momentum in storytelling. Thus, I find myself compelled to examine Tom Bombadil's role and question the necessity of his presence within the epic saga. As we embark on this critical inquiry, let us consider the reasons why Old Tom's playful presence may be seen as a disruptive force.</p><h2>An Intriguing Yet Disjointed Figure</h2><h3>A Divergence from Narrative Flow</h3><p>Tolkien's epic is known for its meticulous pacing and the gravity of its themes. Enter Tom Bombadil – a character whose frivolity and detachment from worldly events create a jarring contrast within the otherwise cohesive narrative:</p><ol><li><b>An Unnecessary Interlude</b>: The encounter with Tom, while quaint and endearing, serves as a temporal diversion that detracts from the urgency of the Fellowship's quest.</li><li><b>An Outlier in Purpose</b>: His escapades, while rich in mirth, add little to the central narrative, raising questions about their relevance in the grand design of Middle-earth.</li></ol><h2>An Enigma that Remains Unresolved</h2><h3>A Break from Coherence</h3><p>In a tale defined by intricate connections and deeply rooted mythology, Bombadil's inexplicable nature poses a challenge to the narrative's internal logic:</p><ul><li><b>A Mystery Without Resolution</b>: Unlike other enigmatic figures whose backstories enrich the tapestry, Tom remains enigmatic, shrouded in mystery that neither advances the plot nor deepens the lore.</li><li><b>A Departure from Tone</b>: His presence, filled with lighthearted songs and whimsical antics, contrasts sharply with the solemnity and tension that define the rest of the saga.</li></ul><pre><code>print("Tom")
|
||||||
|
print("Bombadil")
|
||||||
|
print("A")
|
||||||
|
print("Mystery")
|
||||||
|
</code></pre><h2>A Theme of <b>Disruption</b></h2><h3>An Element of Distraction</h3><p>Tom Bombadil's inclusion inadvertently shifts focus from the pressing matters of Middle-earth, introducing themes that sit uneasily with the narrative's core:</p><ul><li><b>A Shift in Focus</b>: His carefree demeanor and ability to withhold the power of the One Ring, while intriguing, distract from the overarching themes of sacrifice and moral complexity.</li><li><b>A Misstep in Continuity</b>: His segment, charming as it may be, disrupts the journey's continuous build-up towards the looming confrontation with darkness.</li></ul><h2>Conclusion</h2><p>As we ponder the manifold wonders and intricacies of Tolkien's world, it is evident that Tom Bombadil, while delightfully unique, was a narrative anomaly – a whimsical reflection in the mirror of Middle-earth's grand narrative. While his character captivates with a certain mystique, it answers questions that were never asked, leaving readers with more enigmas than revelations.</p><p>In conclusion, as one who has explored the mythic past of Middle-earth and sought coherence in its storied legacy, I propose that Tom Bombadil, for all his merriment and enigma, was a divergence from the tale's destined path – a curiosity that, while endearing to some, stands as a reminder that even in the most meticulously crafted worlds, not all paths lead to the fulfillment of the quest.</p><p>Thus, let us bid farewell to Old Tom with a final song, recognizing both his charm and the discord his presence sowed. For within the hallowed pages of Tolkien's masterpiece, every beat must resonate with purpose, lest the harmony of the tale be lost to idle whimsy.</p></div></article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Contact the Author</title>
|
||||||
|
<link href="/tolkien-fan-club/index.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<article><div><h1>Contact the Author</h1><p><a href="/tolkien-fan-club/">< Back Home</a></p><p>Give me a call anytime to chat about Tolkien!</p><p><code>555-555-5555</code></p><p><b>"Váya márië."</b></p></div></article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 663 KiB |
|
After Width: | Height: | Size: 500 KiB |
@@ -0,0 +1,130 @@
|
|||||||
|
body {
|
||||||
|
background-color: #1f1c25;
|
||||||
|
color: #f0e6d1;
|
||||||
|
font-family: "Luminari", "Georgia", serif;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
color: #dda15e;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-shadow: 2px 2px 4px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #e0a96d;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 2px solid #e0a96d;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #f4a261;
|
||||||
|
border-color: #f4a261;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: #3c3c42;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #e9c46a;
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #3c3c42;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1em;
|
||||||
|
overflow: auto;
|
||||||
|
box-shadow: 2px 2px 6px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
background-color: #2e2c35;
|
||||||
|
border-left: 4px solid #8d99ae;
|
||||||
|
padding-left: 2em;
|
||||||
|
margin-left: 0;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
color: #ddd;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 3px solid #3c3c42;
|
||||||
|
box-shadow: 3px 3px 6px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #1f1c25;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #3c3c42;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 3px solid #1f1c25;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #5a5466;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #3c3c42 #1f1c25;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: #1f1c25;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Tolkien Fan Club</title>
|
||||||
|
<link href="/tolkien-fan-club/index.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<article><div><h1>Tolkien Fan Club</h1><p><img src="/tolkien-fan-club/images/tolkien.png" alt="JRR Tolkien sitting"></img></p><p>Here's the deal, <b>I like Tolkien</b>.</p><blockquote>"I am in fact a Hobbit in all but size."
|
||||||
|
|
||||||
|
-- J.R.R. Tolkien</blockquote><h2>Blog posts</h2><ul><li><a href="/tolkien-fan-club/blog/glorfindel">Why Glorfindel is More Impressive than Legolas</a></li><li><a href="/tolkien-fan-club/blog/tom">Why Tom Bombadil Was a Mistake</a></li><li><a href="/tolkien-fan-club/blog/majesty">The Unparalleled Majesty of "The Lord of the Rings"</a></li></ul><h2>Reasons I like Tolkien</h2><ul><li>You can spend years studying the legendarium and still not understand its depths</li><li>It can be enjoyed by children and adults alike</li><li>Disney <i>didn't ruin it</i> (okay, but Amazon might have)</li><li>It created an entirely new genre of fantasy</li></ul><h2>My favorite characters (in order)</h2><ol><li>Gandalf</li><li>Bilbo</li><li>Sam</li><li>Glorfindel</li><li>Galadriel</li><li>Elrond</li><li>Thorin</li><li>Sauron</li><li>Aragorn</li></ol><p>Here's what <code>elflang</code> looks like (the perfect coding language):</p><pre><code>func main(){
|
||||||
|
fmt.Println("Aiya, Ambar!")
|
||||||
|
}
|
||||||
|
</code></pre><p>Want to get in touch? <a href="/tolkien-fan-club/contact">Contact me here</a>.</p><p>This site was generated with a custom-built <a href="https://www.boot.dev/courses/build-static-site-generator-python">static site generator</a> from the course on <a href="https://www.boot.dev">Boot.dev</a>.</p></div></article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
python3 src/main.py "public" "/"
|
||||||
|
cd public && python3 -m http.server 8888
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<div><pre><code>This is text that _should_ remain
|
||||||
|
the **same** even with inline stuff
|
||||||
|
</code></pre></div>
|
||||||
|
|
||||||
|
<div><pre><code>This is text that _should_ remain
|
||||||
|
the **same** even with inline stuff
|
||||||
|
</code></pre></div>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[project]
|
||||||
|
name = "tolkien-fan-club"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A static site built with a static site generator."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = []
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import re
|
||||||
|
import os
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
|
||||||
|
class BlockType(Enum):
|
||||||
|
PARAGRAPH = "paragraph"
|
||||||
|
HEADING = "heading"
|
||||||
|
CODE = "code"
|
||||||
|
QUOTE = "quote"
|
||||||
|
ORDERED_LIST = "ordered list"
|
||||||
|
UNORDERED_LIST = "unordered list"
|
||||||
|
|
||||||
|
def block_to_block_type(block: str) -> BlockType:
|
||||||
|
if is_heading(block):
|
||||||
|
return BlockType.HEADING
|
||||||
|
|
||||||
|
if is_code(block):
|
||||||
|
return BlockType.CODE
|
||||||
|
|
||||||
|
if is_quote(block):
|
||||||
|
return BlockType.QUOTE
|
||||||
|
|
||||||
|
if is_unordered(block):
|
||||||
|
return BlockType.UNORDERED_LIST
|
||||||
|
|
||||||
|
if is_ordered(block):
|
||||||
|
return BlockType.ORDERED_LIST
|
||||||
|
|
||||||
|
return BlockType.PARAGRAPH
|
||||||
|
|
||||||
|
def is_heading(block: str) -> bool:
|
||||||
|
pattern = r"^#{1,6}\s{1}?"
|
||||||
|
return has_pattern(pattern, block)
|
||||||
|
|
||||||
|
def is_code(block: str) -> bool:
|
||||||
|
pattern = rf"^```.*{os.linesep}[\s\S]*{os.linesep}```$"
|
||||||
|
return has_pattern(pattern, block)
|
||||||
|
|
||||||
|
def is_quote(block: str) -> bool:
|
||||||
|
pattern = r"^>"
|
||||||
|
return each_line_has_pattern(lambda _: pattern, block)
|
||||||
|
|
||||||
|
def is_unordered(block: str) -> bool:
|
||||||
|
pattern = r"^-\s"
|
||||||
|
return each_line_has_pattern(lambda _: pattern, block)
|
||||||
|
|
||||||
|
def is_ordered(block: str) -> bool:
|
||||||
|
return each_line_has_pattern(lambda i: rf"{i + 1}\.\s", block)
|
||||||
|
|
||||||
|
def has_pattern(pattern: str, block: str) -> bool:
|
||||||
|
return re.match(pattern, block) != None
|
||||||
|
|
||||||
|
def each_line_has_pattern(pattern_builder: Callable[[int], str], block: str) -> bool:
|
||||||
|
lines = block.split(os.linesep)
|
||||||
|
|
||||||
|
for i in range(len(lines)):
|
||||||
|
line = lines[i]
|
||||||
|
|
||||||
|
if line == '':
|
||||||
|
continue
|
||||||
|
|
||||||
|
pattern = pattern_builder(i)
|
||||||
|
if not has_pattern(pattern, line):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
def extract_markdown_images(text: str) -> list[tuple[str, str]]:
|
||||||
|
return extract(r"!\[(.+?)\]\((.+?)\)", text)
|
||||||
|
|
||||||
|
def extract_markdown_links(text: str) -> list[tuple[str, str]]:
|
||||||
|
return extract(r"(?<!!)\[(.+?)\]\((.+?)\)", text)
|
||||||
|
|
||||||
|
def extract(pattern: str, text: str):
|
||||||
|
return re.findall(pattern, text)
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from typing import Self
|
||||||
|
|
||||||
|
|
||||||
|
class HtmlNode:
|
||||||
|
def __init__(
|
||||||
|
self: Self,
|
||||||
|
tag: str | None = None,
|
||||||
|
value: str | None = None,
|
||||||
|
children: list["HtmlNode"] | None = None,
|
||||||
|
props: dict | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.tag = tag
|
||||||
|
self.value = value
|
||||||
|
self.children = children
|
||||||
|
self.props = props
|
||||||
|
|
||||||
|
def __repr__(self: Self) -> str:
|
||||||
|
return f"{HtmlNode.__name__}({self.tag}, {self.value}, {self.children}, {self.props})"
|
||||||
|
|
||||||
|
def to_html(self: Self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def props_to_html(self: Self) -> str:
|
||||||
|
if self.props is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for prop in self.props:
|
||||||
|
result.append(f' {prop}="{self.props[prop]}"')
|
||||||
|
|
||||||
|
return "".join(result)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from typing import Self
|
||||||
|
|
||||||
|
from htmlnode import HtmlNode
|
||||||
|
|
||||||
|
|
||||||
|
class LeafNode(HtmlNode):
|
||||||
|
def __init__(
|
||||||
|
self: Self,
|
||||||
|
tag: str | None,
|
||||||
|
value: str | None,
|
||||||
|
props: dict | None = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(tag, value, [], props)
|
||||||
|
|
||||||
|
def to_html(self: Self) -> str:
|
||||||
|
if self.value is None:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
if self.tag is None:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
return f"<{self.tag}{self.props_to_html()}>{self.value}</{self.tag}>"
|
||||||
|
|
||||||
|
def __repr__(self: Self) -> str:
|
||||||
|
return f"{LeafNode.__name__}({self.tag}, {self.value}, {self.props})"
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from splitnodes import markdown_to_html_node
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
raise RuntimeError("Please provide destination and basepath: <destination> <base_path>")
|
||||||
|
|
||||||
|
destination_dir = sys.argv[1]
|
||||||
|
basepath = sys.argv[2]
|
||||||
|
|
||||||
|
copy_static(destination_dir)
|
||||||
|
generate_pages("content", "template.html", destination_dir, basepath)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_static(destination: str) -> None:
|
||||||
|
source_path = Path("static")
|
||||||
|
destination_path = Path(destination)
|
||||||
|
|
||||||
|
if not source_path.exists():
|
||||||
|
raise FileNotFoundError(f"Source directory '{source_path}' does not exist.")
|
||||||
|
|
||||||
|
if destination_path.exists():
|
||||||
|
print(f"Cleaning contents of '{destination_path}'...")
|
||||||
|
|
||||||
|
for item in destination_path.iterdir():
|
||||||
|
if item.is_dir():
|
||||||
|
shutil.rmtree(item)
|
||||||
|
else:
|
||||||
|
item.unlink()
|
||||||
|
else:
|
||||||
|
destination_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"Copying files from '{source_path}' to '{destination_path}'...")
|
||||||
|
|
||||||
|
for item in source_path.rglob("*"):
|
||||||
|
relative_path = item.relative_to(source_path)
|
||||||
|
target_path = destination_path / relative_path
|
||||||
|
|
||||||
|
if item.is_dir():
|
||||||
|
target_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
else:
|
||||||
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(item, target_path)
|
||||||
|
|
||||||
|
print(f"Finished copying files from '{source_path}' to '{destination_path}'")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_pages(
|
||||||
|
dir_path_content: str, template_path: str, dest_dir_path: str, basepath: str
|
||||||
|
) -> None:
|
||||||
|
content_dir = Path(dir_path_content)
|
||||||
|
template = Path(template_path)
|
||||||
|
dest_dir = Path(dest_dir_path)
|
||||||
|
|
||||||
|
print(f"Crawling '{content_dir}' to generate HTML pages in '{dest_dir}'...")
|
||||||
|
|
||||||
|
for item in content_dir.rglob("*"):
|
||||||
|
if item.is_file() and item.suffix.lower() == ".md":
|
||||||
|
relative_path = item.relative_to(content_dir)
|
||||||
|
target_path = dest_dir / relative_path.with_suffix(".html")
|
||||||
|
|
||||||
|
generate_page(item, template, target_path, basepath)
|
||||||
|
|
||||||
|
print("Page generation complete")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_page(
|
||||||
|
from_path: str | Path, template_path: str | Path, dest_path: str | Path, basepath: str
|
||||||
|
) -> None:
|
||||||
|
src = Path(from_path)
|
||||||
|
template = Path(template_path)
|
||||||
|
dest = Path(dest_path)
|
||||||
|
|
||||||
|
if not src.exists():
|
||||||
|
raise RuntimeError(f"Source file '{src}' does not exist")
|
||||||
|
|
||||||
|
if not src.is_file():
|
||||||
|
raise RuntimeError(f"Source file '{src}' is not a file")
|
||||||
|
|
||||||
|
if not template.exists():
|
||||||
|
raise RuntimeError(f"Template file '{template}' does not exist")
|
||||||
|
|
||||||
|
if not template.is_file():
|
||||||
|
raise RuntimeError(f"Template file '{template}' is not a file")
|
||||||
|
|
||||||
|
print(f"Generating page from '{src}' to '{dest}' using '{template}'")
|
||||||
|
|
||||||
|
markdown_content = src.read_text(encoding="utf-8")
|
||||||
|
template_content = template.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
html_node = markdown_to_html_node(markdown_content)
|
||||||
|
|
||||||
|
title = extract_title(markdown_content)
|
||||||
|
html = html_node.to_html()
|
||||||
|
|
||||||
|
rendered_html = template_content.replace("{{ Title }}", title)
|
||||||
|
rendered_html = rendered_html.replace("{{ Content }}", html)
|
||||||
|
rendered_html = rendered_html.replace('href="/', f'href="{basepath}')
|
||||||
|
rendered_html = rendered_html.replace('src="/', f'src="{basepath}')
|
||||||
|
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
dest.write_text(rendered_html, encoding="utf-8")
|
||||||
|
print(f"Page successfuly generated: 'dest'")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_title(markdown: str) -> str:
|
||||||
|
heading_identifier = "# "
|
||||||
|
lines = markdown.splitlines()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith(heading_identifier):
|
||||||
|
return line.lstrip(heading_identifier)
|
||||||
|
|
||||||
|
raise RuntimeError("No heading found that can be used as title")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from typing import Self
|
||||||
|
|
||||||
|
from htmlnode import HtmlNode
|
||||||
|
|
||||||
|
|
||||||
|
class ParentNode(HtmlNode):
|
||||||
|
def __init__(
|
||||||
|
self: Self,
|
||||||
|
tag: str,
|
||||||
|
children: list[HtmlNode],
|
||||||
|
value: str | None = None,
|
||||||
|
props: dict | None = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(tag, value, children, props)
|
||||||
|
|
||||||
|
def to_html(self: Self) -> str:
|
||||||
|
if self.tag is None:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
if self.children is None or len(self.children) == 0:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
html = []
|
||||||
|
html.append(f"<{self.tag}{self.props_to_html()}>")
|
||||||
|
|
||||||
|
for child in self.children:
|
||||||
|
html.append(child.to_html())
|
||||||
|
|
||||||
|
html.append(f"</{self.tag}>")
|
||||||
|
|
||||||
|
return "".join(html)
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import os
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from block import BlockType, block_to_block_type
|
||||||
|
from extract import extract_markdown_images, extract_markdown_links
|
||||||
|
from htmlnode import HtmlNode
|
||||||
|
from parentnode import ParentNode
|
||||||
|
from textnode import TextNode, TextType, text_node_to_html_node
|
||||||
|
|
||||||
|
|
||||||
|
def split_nodes_delimiter(
|
||||||
|
old_nodes: list[TextNode], delimiter: str, text_type: TextType
|
||||||
|
) -> list[TextNode]:
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for node in old_nodes:
|
||||||
|
if node.text_type != TextType.PLAIN:
|
||||||
|
result.append(node)
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = node.text.split(delimiter)
|
||||||
|
|
||||||
|
if len(parts) % 2 == 0:
|
||||||
|
raise RuntimeError("No matching delimiter found")
|
||||||
|
|
||||||
|
for i in range(len(parts)):
|
||||||
|
current_text = parts[i]
|
||||||
|
|
||||||
|
if i % 2 == 0:
|
||||||
|
result.append(TextNode(current_text, TextType.PLAIN))
|
||||||
|
else:
|
||||||
|
result.append(TextNode(current_text, text_type))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def split_nodes_image(old_nodes: list[TextNode]) -> list[TextNode]:
|
||||||
|
return split_nodes(
|
||||||
|
old_nodes,
|
||||||
|
extracter=extract_markdown_images,
|
||||||
|
pattern_builder=lambda t: f"![{t[0]}]({t[1]})",
|
||||||
|
node_creator=lambda t: TextNode(t[0], TextType.IMAGE, t[1]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def split_nodes_link(old_nodes: list[TextNode]) -> list[TextNode]:
|
||||||
|
return split_nodes(
|
||||||
|
old_nodes,
|
||||||
|
extracter=extract_markdown_links,
|
||||||
|
pattern_builder=lambda t: f"[{t[0]}]({t[1]})",
|
||||||
|
node_creator=lambda t: TextNode(t[0], TextType.LINK, t[1]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def split_nodes(
|
||||||
|
old_nodes: list[TextNode],
|
||||||
|
extracter: Callable[[str], list[tuple[str, str]]],
|
||||||
|
pattern_builder: Callable[[tuple[str, str]], str],
|
||||||
|
node_creator: Callable[[tuple[str, str]], TextNode],
|
||||||
|
) -> list[TextNode]:
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for node in old_nodes:
|
||||||
|
if node.text_type != TextType.PLAIN:
|
||||||
|
result.append(node)
|
||||||
|
continue
|
||||||
|
|
||||||
|
extracted = extracter(node.text)
|
||||||
|
|
||||||
|
node_text = node.text
|
||||||
|
|
||||||
|
for extract in extracted:
|
||||||
|
pattern = pattern_builder(extract)
|
||||||
|
parts = node_text.split(pattern, maxsplit=1)
|
||||||
|
|
||||||
|
result.append(TextNode(parts[0], TextType.PLAIN))
|
||||||
|
|
||||||
|
node = node_creator(extract)
|
||||||
|
result.append(node)
|
||||||
|
|
||||||
|
node_text = parts[1]
|
||||||
|
|
||||||
|
if len(node_text) > 0:
|
||||||
|
result.append(TextNode(node_text, TextType.PLAIN))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def text_to_text_node(text: str) -> list[TextNode]:
|
||||||
|
old_nodes = [TextNode(text, TextType.PLAIN)]
|
||||||
|
old_nodes = split_nodes_delimiter(old_nodes, "**", TextType.BOLD)
|
||||||
|
old_nodes = split_nodes_delimiter(old_nodes, "_", TextType.ITALIC)
|
||||||
|
old_nodes = split_nodes_delimiter(old_nodes, "`", TextType.CODE)
|
||||||
|
old_nodes = split_nodes_image(old_nodes)
|
||||||
|
old_nodes = split_nodes_link(old_nodes)
|
||||||
|
return old_nodes
|
||||||
|
|
||||||
|
|
||||||
|
def markdown_to_blocks(markdown: str) -> list[str]:
|
||||||
|
blocks = markdown.split(
|
||||||
|
"\n\n",
|
||||||
|
)
|
||||||
|
trimmed = [block.strip() for block in blocks]
|
||||||
|
non_empty = [block for block in trimmed if block != ""]
|
||||||
|
return non_empty
|
||||||
|
|
||||||
|
|
||||||
|
def markdown_to_html_node(markdown: str) -> HtmlNode:
|
||||||
|
children = []
|
||||||
|
blocks = markdown_to_blocks(markdown)
|
||||||
|
|
||||||
|
for block in blocks:
|
||||||
|
block_type = block_to_block_type(block)
|
||||||
|
|
||||||
|
match block_type:
|
||||||
|
case BlockType.PARAGRAPH:
|
||||||
|
no_newlines = block.replace(os.linesep, " ")
|
||||||
|
paragraph_children = text_to_children(no_newlines)
|
||||||
|
paragraph_node = ParentNode("p", children=paragraph_children)
|
||||||
|
children.append(paragraph_node)
|
||||||
|
case BlockType.HEADING:
|
||||||
|
heading_number = 0
|
||||||
|
|
||||||
|
for c in block:
|
||||||
|
if c != "#":
|
||||||
|
break
|
||||||
|
|
||||||
|
heading_number += 1
|
||||||
|
|
||||||
|
heading_children = text_to_children(block.lstrip(f"{"#" * heading_number} "))
|
||||||
|
heading_node = ParentNode(
|
||||||
|
f"h{heading_number}", children=heading_children
|
||||||
|
)
|
||||||
|
children.append(heading_node)
|
||||||
|
case BlockType.CODE:
|
||||||
|
removed = block.lstrip("```\n").rstrip("```")
|
||||||
|
text_node = TextNode(removed, TextType.CODE)
|
||||||
|
code_node = text_node_to_html_node(text_node)
|
||||||
|
pre_children: list[HtmlNode] = [code_node]
|
||||||
|
pre_node = ParentNode("pre", children=pre_children)
|
||||||
|
children.append(pre_node)
|
||||||
|
case BlockType.QUOTE:
|
||||||
|
replaced_blocks = []
|
||||||
|
|
||||||
|
lines = block.splitlines()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
replaced = line.lstrip("> ").lstrip(">")
|
||||||
|
replaced_blocks.append(replaced)
|
||||||
|
|
||||||
|
replaced_block = os.linesep.join(replaced_blocks)
|
||||||
|
quote_children = text_to_children(replaced_block)
|
||||||
|
block_node = ParentNode("blockquote", children=quote_children)
|
||||||
|
children.append(block_node)
|
||||||
|
case BlockType.ORDERED_LIST:
|
||||||
|
ordered_list_children = []
|
||||||
|
ordered_list_items = block.splitlines()
|
||||||
|
|
||||||
|
for i in range(len(ordered_list_items)):
|
||||||
|
ordered_item = ordered_list_items[i]
|
||||||
|
ordered_item_children_nodes = text_to_children(ordered_item.lstrip(f"{i + 1}. "))
|
||||||
|
ordered_item_node = ParentNode(
|
||||||
|
"li", children=ordered_item_children_nodes
|
||||||
|
)
|
||||||
|
ordered_list_children.append(ordered_item_node)
|
||||||
|
|
||||||
|
ordered_list_node = ParentNode("ol", children=ordered_list_children)
|
||||||
|
children.append(ordered_list_node)
|
||||||
|
case BlockType.UNORDERED_LIST:
|
||||||
|
unordered_list_children = []
|
||||||
|
unordered_list_items = block.splitlines()
|
||||||
|
|
||||||
|
for unordered_item in unordered_list_items:
|
||||||
|
unordered_item_children_nodes = text_to_children(unordered_item.lstrip("- "))
|
||||||
|
unordered_item_node = ParentNode(
|
||||||
|
"li", children=unordered_item_children_nodes
|
||||||
|
)
|
||||||
|
unordered_list_children.append(unordered_item_node)
|
||||||
|
|
||||||
|
unordered_list_node = ParentNode("ul", children=unordered_list_children)
|
||||||
|
children.append(unordered_list_node)
|
||||||
|
case _:
|
||||||
|
raise NotImplemented("Unknown block type")
|
||||||
|
|
||||||
|
return ParentNode("div", children=children)
|
||||||
|
|
||||||
|
|
||||||
|
def text_to_children(markdown: str) -> list[HtmlNode]:
|
||||||
|
children = []
|
||||||
|
|
||||||
|
text_nodes = text_to_text_node(markdown)
|
||||||
|
|
||||||
|
for text_node in text_nodes:
|
||||||
|
html_node = text_node_to_html_node(text_node)
|
||||||
|
children.append(html_node)
|
||||||
|
|
||||||
|
return children
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from block import BlockType, block_to_block_type
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlock(unittest.TestCase):
|
||||||
|
def test_block_to_block_type_when_given_heading_it_should_return_heading(self):
|
||||||
|
cases = [
|
||||||
|
{"input": "# Importante", "expected": BlockType.HEADING},
|
||||||
|
{"input": "## Importante", "expected": BlockType.HEADING},
|
||||||
|
{"input": "### Importante", "expected": BlockType.HEADING},
|
||||||
|
{"input": "#### Importante", "expected": BlockType.HEADING},
|
||||||
|
{"input": "##### Importante", "expected": BlockType.HEADING},
|
||||||
|
{"input": "###### Importante", "expected": BlockType.HEADING},
|
||||||
|
]
|
||||||
|
|
||||||
|
for case in cases:
|
||||||
|
with self.subTest(inputs=case["input"]):
|
||||||
|
result = block_to_block_type(case["input"])
|
||||||
|
self.assertEqual(result, case["expected"])
|
||||||
|
|
||||||
|
def test_block_to_block_type_when_given_code_it_should_return_code(self):
|
||||||
|
cases = [
|
||||||
|
{
|
||||||
|
"input": "```\nconsole.log('hello world')\n```",
|
||||||
|
"expected": BlockType.CODE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "```\nconsole.log('hello world');\nfunction(num) {\n\treturn num;\n}\n```",
|
||||||
|
"expected": BlockType.CODE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "```js\nconsole.log('hello world');\nfunction(num) {\n\treturn num;\n}\n```",
|
||||||
|
"expected": BlockType.CODE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "```typescript\nconsole.log('hello world');\nfunction(num) {\n\treturn num;\n}\n```",
|
||||||
|
"expected": BlockType.CODE,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for case in cases:
|
||||||
|
with self.subTest(inputs=case["input"]):
|
||||||
|
result = block_to_block_type(case["input"])
|
||||||
|
self.assertEqual(result, case["expected"])
|
||||||
|
|
||||||
|
def test_block_to_block_type_when_given_quote_it_should_return_quote(self):
|
||||||
|
cases = [
|
||||||
|
{
|
||||||
|
"input": "> this is\n> a quote\n>about stuff",
|
||||||
|
"expected": BlockType.QUOTE,
|
||||||
|
},
|
||||||
|
{"input": ">this is\n", "expected": BlockType.QUOTE},
|
||||||
|
{"input": "> this is", "expected": BlockType.QUOTE},
|
||||||
|
{"input": ">this is", "expected": BlockType.QUOTE},
|
||||||
|
]
|
||||||
|
|
||||||
|
for case in cases:
|
||||||
|
with self.subTest(inputs=case["input"]):
|
||||||
|
result = block_to_block_type(case["input"])
|
||||||
|
self.assertEqual(result, case["expected"])
|
||||||
|
|
||||||
|
def test_block_to_block_type_when_given_unordered_it_should_return_unordered(self):
|
||||||
|
cases = [
|
||||||
|
{
|
||||||
|
"input": "- item one\n- item two\n- item three",
|
||||||
|
"expected": BlockType.UNORDERED_LIST,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for case in cases:
|
||||||
|
with self.subTest(inputs=case["input"]):
|
||||||
|
result = block_to_block_type(case["input"])
|
||||||
|
self.assertEqual(result, case["expected"])
|
||||||
|
|
||||||
|
def test_block_to_block_type_when_given_ordered_it_should_return_unordered(self):
|
||||||
|
cases = [
|
||||||
|
{
|
||||||
|
"input": "1. item one\n2. item two\n3. item three",
|
||||||
|
"expected": BlockType.ORDERED_LIST,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for case in cases:
|
||||||
|
with self.subTest(inputs=case["input"]):
|
||||||
|
result = block_to_block_type(case["input"])
|
||||||
|
self.assertEqual(result, case["expected"])
|
||||||
|
|
||||||
|
def test_block_to_block_type_when_given_paragraph_it_should_return_paragraph(self):
|
||||||
|
cases = [
|
||||||
|
{
|
||||||
|
"input": "This is a paragraph about nothing.",
|
||||||
|
"expected": BlockType.PARAGRAPH,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for case in cases:
|
||||||
|
with self.subTest(inputs=case["input"]):
|
||||||
|
result = block_to_block_type(case["input"])
|
||||||
|
self.assertEqual(result, case["expected"])
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from extract import extract_markdown_images, extract_markdown_links
|
||||||
|
|
||||||
|
|
||||||
|
class TextExtract(unittest.TestCase):
|
||||||
|
def test_extract_markdown_images_when_given_no_images_it_should_return_empty_list(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
result = extract_markdown_images("")
|
||||||
|
|
||||||
|
self.assertListEqual(result, [])
|
||||||
|
|
||||||
|
def test_extract_markdown_images_when_given_images_it_should_return_list(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
expected = [
|
||||||
|
("rick roll", "https://i.imgur.com/aKaOqIh.gif"),
|
||||||
|
("obi wan", "https://i.imgur.com/fJRm4Vk.jpeg")
|
||||||
|
]
|
||||||
|
|
||||||
|
result = extract_markdown_images("This is text with a  and ")
|
||||||
|
|
||||||
|
self.assertListEqual(result, expected)
|
||||||
|
|
||||||
|
def test_extract_markdown_links_when_given_no_links_it_should_return_empty_list(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
result = extract_markdown_links("")
|
||||||
|
|
||||||
|
self.assertListEqual(result, [])
|
||||||
|
|
||||||
|
def test_extract_markdown_links_when_given_links_it_should_return_list(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
expected = [
|
||||||
|
("to boot dev", "https://www.boot.dev"),
|
||||||
|
("to youtube", "https://www.youtube.com/@bootdotdev")
|
||||||
|
]
|
||||||
|
|
||||||
|
result = extract_markdown_links("This is text with a link [to boot dev](https://www.boot.dev) and [to youtube](https://www.youtube.com/@bootdotdev)")
|
||||||
|
|
||||||
|
self.assertListEqual(result, expected)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from htmlnode import HtmlNode
|
||||||
|
|
||||||
|
|
||||||
|
class TestHtmlNode(unittest.TestCase):
|
||||||
|
def test_it_should_not_require_any_properties(self):
|
||||||
|
node = HtmlNode()
|
||||||
|
|
||||||
|
self.assertIsNone(node.tag)
|
||||||
|
self.assertIsNone(node.value)
|
||||||
|
self.assertIsNone(node.children)
|
||||||
|
self.assertIsNone(node.props)
|
||||||
|
|
||||||
|
def test_it_should_have_properties(self):
|
||||||
|
tag = "tag"
|
||||||
|
value = "value"
|
||||||
|
children = [HtmlNode()]
|
||||||
|
props = {}
|
||||||
|
|
||||||
|
node = HtmlNode(tag, value, children, props)
|
||||||
|
|
||||||
|
self.assertEqual(node.tag, tag)
|
||||||
|
self.assertEqual(node.value, value)
|
||||||
|
|
||||||
|
assert node.children is not None
|
||||||
|
self.assertListEqual(node.children, children)
|
||||||
|
|
||||||
|
assert node.props is not None
|
||||||
|
self.assertDictEqual(node.props, props)
|
||||||
|
|
||||||
|
def test_to_html_when_called_it_should_raise_not_implemented_error(self):
|
||||||
|
node = HtmlNode()
|
||||||
|
|
||||||
|
with self.assertRaises(NotImplementedError):
|
||||||
|
node.to_html()
|
||||||
|
|
||||||
|
def test_props_to_html_when_called_and_props_none_it_should_return_empty_string(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
node = HtmlNode()
|
||||||
|
|
||||||
|
result = node.props_to_html()
|
||||||
|
|
||||||
|
self.assertEqual(result, "")
|
||||||
|
|
||||||
|
def test_props_to_html_when_called_with_props_it_should_return_in_proper_format(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
node = HtmlNode(props={"href": "https://www.google.com", "target": "_blank"})
|
||||||
|
|
||||||
|
result = node.props_to_html()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
' href="https://www.google.com" target="_blank"',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_repr_when_called_it_should_return_expected_representation(self):
|
||||||
|
tag = "tag"
|
||||||
|
value = "value"
|
||||||
|
children = [HtmlNode()]
|
||||||
|
props = {}
|
||||||
|
|
||||||
|
node = HtmlNode(tag, value, children, props)
|
||||||
|
|
||||||
|
result = repr(node)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
result, f"{HtmlNode.__name__}({tag}, {value}, {children}, {props})"
|
||||||
|
)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from leafnode import LeafNode
|
||||||
|
|
||||||
|
|
||||||
|
class LeafNodeTest(unittest.TestCase):
|
||||||
|
def test_to_html_when_leaf_no_value_it_should_raise_value_error(self):
|
||||||
|
node = LeafNode("p", None)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
node.to_html()
|
||||||
|
|
||||||
|
def test_to_html_when_no_tag_it_should_return_value(self):
|
||||||
|
expected = "Hello Chat"
|
||||||
|
node = LeafNode(None, expected)
|
||||||
|
|
||||||
|
result = node.to_html()
|
||||||
|
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_to_html_when_tag_and_value_it_should_return_html_tag(self):
|
||||||
|
tag = "h1"
|
||||||
|
value = "Hello Chat"
|
||||||
|
expected = f"<{tag}>{value}</{tag}>"
|
||||||
|
|
||||||
|
node = LeafNode(None, expected)
|
||||||
|
|
||||||
|
result = node.to_html()
|
||||||
|
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_repr_when_called_it_should_return_expected_representation(self):
|
||||||
|
tag = "tag"
|
||||||
|
value = "value"
|
||||||
|
props = {}
|
||||||
|
|
||||||
|
node = LeafNode(tag, value, props)
|
||||||
|
|
||||||
|
result = repr(node)
|
||||||
|
|
||||||
|
self.assertEqual(result, f"{LeafNode.__name__}({tag}, {value}, {props})")
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import unittest
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from htmlnode import HtmlNode
|
||||||
|
from leafnode import LeafNode
|
||||||
|
from parentnode import ParentNode
|
||||||
|
|
||||||
|
|
||||||
|
class TestParentNode(unittest.TestCase):
|
||||||
|
def test_to_html_when_no_tag_it_should_raise_value_error(self):
|
||||||
|
node = ParentNode(cast(str, None), [LeafNode("p", "test")])
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
node.to_html()
|
||||||
|
|
||||||
|
def test_to_html_when_no_children_it_should_raise_value_error(self):
|
||||||
|
node = ParentNode("a", cast(list[HtmlNode], None))
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
node.to_html()
|
||||||
|
|
||||||
|
def test_to_html_when_children_is_empty_it_should_raise_value_error(self):
|
||||||
|
node = ParentNode("a", [])
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
node.to_html()
|
||||||
|
|
||||||
|
def test_to_html_when_tag_and_children_it_should_return_html(self):
|
||||||
|
expected = "<p><b>Bold text</b>Normal text<i>italic text</i>Normal text</p>"
|
||||||
|
|
||||||
|
node = ParentNode(
|
||||||
|
"p",
|
||||||
|
[
|
||||||
|
LeafNode("b", "Bold text"),
|
||||||
|
LeafNode(None, "Normal text"),
|
||||||
|
LeafNode("i", "italic text"),
|
||||||
|
LeafNode(None, "Normal text"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
result = node.to_html()
|
||||||
|
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_to_html_when_with_children_it_should_return_html(self):
|
||||||
|
child_node = LeafNode("span", "child")
|
||||||
|
parent_node = ParentNode("div", [child_node])
|
||||||
|
self.assertEqual(parent_node.to_html(), "<div><span>child</span></div>")
|
||||||
|
|
||||||
|
def test_to_html_with_grandchildren_it_should_return_html(self):
|
||||||
|
grandchild_node = LeafNode("b", "grandchild")
|
||||||
|
child_node = ParentNode("span", [grandchild_node])
|
||||||
|
parent_node = ParentNode("div", [child_node])
|
||||||
|
self.assertEqual(
|
||||||
|
parent_node.to_html(),
|
||||||
|
"<div><span><b>grandchild</b></span></div>",
|
||||||
|
)
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from splitnodes import (markdown_to_blocks, markdown_to_html_node,
|
||||||
|
split_nodes_delimiter, split_nodes_image,
|
||||||
|
split_nodes_link, text_to_text_node)
|
||||||
|
from textnode import TextNode, TextType
|
||||||
|
|
||||||
|
|
||||||
|
class TestSplitNodes(unittest.TestCase):
|
||||||
|
def test_split_nodes_delimiter_when_given_empty_list_it_should_return_empty_list(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
expected = []
|
||||||
|
|
||||||
|
result = split_nodes_delimiter([], "`", TextType.CODE)
|
||||||
|
|
||||||
|
self.assertListEqual(result, expected)
|
||||||
|
|
||||||
|
def test_split_nodes_delimiter_when_given_no_plain_type_it_should_return_with_no_changes(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
code_node = TextNode("test", TextType.CODE)
|
||||||
|
expected = [code_node]
|
||||||
|
|
||||||
|
result = split_nodes_delimiter([code_node], "`", TextType.CODE)
|
||||||
|
|
||||||
|
self.assertListEqual(result, expected)
|
||||||
|
|
||||||
|
def test_split_nodes_delimiter_when_given_no_matching_delimiter_it_should_raise_error(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
text_node = TextNode("This is text with a `code", TextType.PLAIN)
|
||||||
|
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
split_nodes_delimiter([text_node], "`", TextType.CODE)
|
||||||
|
|
||||||
|
def test_split_nodes_delimiter_when_given_bold_input_it_should_return_expected_list(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
expected = [
|
||||||
|
TextNode("This is text with a ", TextType.PLAIN),
|
||||||
|
TextNode("bolded phrase", TextType.BOLD),
|
||||||
|
TextNode(" in the middle", TextType.PLAIN),
|
||||||
|
]
|
||||||
|
|
||||||
|
text_node = TextNode(
|
||||||
|
"This is text with a **bolded phrase** in the middle", TextType.PLAIN
|
||||||
|
)
|
||||||
|
|
||||||
|
result = split_nodes_delimiter([text_node], "**", TextType.BOLD)
|
||||||
|
|
||||||
|
self.assertListEqual(result, expected)
|
||||||
|
|
||||||
|
def test_split_nodes_delimiter_when_given_italics_input_it_should_return_expected_list(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
expected = [
|
||||||
|
TextNode("This is text with a ", TextType.PLAIN),
|
||||||
|
TextNode("italicized phrase", TextType.ITALIC),
|
||||||
|
TextNode(" in the middle", TextType.PLAIN),
|
||||||
|
]
|
||||||
|
|
||||||
|
text_node = TextNode(
|
||||||
|
"This is text with a _italicized phrase_ in the middle", TextType.PLAIN
|
||||||
|
)
|
||||||
|
|
||||||
|
result = split_nodes_delimiter([text_node], "_", TextType.ITALIC)
|
||||||
|
|
||||||
|
self.assertListEqual(result, expected)
|
||||||
|
|
||||||
|
def test_split_nodes_delimiter_when_given_code_input_it_should_return_expected_list(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
expected = [
|
||||||
|
TextNode("This is text with a ", TextType.PLAIN),
|
||||||
|
TextNode("code phrase", TextType.CODE),
|
||||||
|
TextNode(" in the middle", TextType.PLAIN),
|
||||||
|
]
|
||||||
|
|
||||||
|
text_node = TextNode(
|
||||||
|
"This is text with a `code phrase` in the middle", TextType.PLAIN
|
||||||
|
)
|
||||||
|
|
||||||
|
result = split_nodes_delimiter([text_node], "`", TextType.CODE)
|
||||||
|
|
||||||
|
self.assertListEqual(result, expected)
|
||||||
|
|
||||||
|
def test_split_nodes_image_when_given_image_it_should_return_expected_list(self):
|
||||||
|
expected = [
|
||||||
|
TextNode("This is text with an ", TextType.PLAIN),
|
||||||
|
TextNode("image", TextType.IMAGE, "https://i.imgur.com/zjjcJKZ.png"),
|
||||||
|
TextNode(" and another ", TextType.PLAIN),
|
||||||
|
TextNode("second image", TextType.IMAGE, "https://i.imgur.com/3elNhQu.png"),
|
||||||
|
]
|
||||||
|
|
||||||
|
node = TextNode(
|
||||||
|
"This is text with an  and another ",
|
||||||
|
TextType.PLAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = split_nodes_image([node])
|
||||||
|
|
||||||
|
self.assertListEqual(result, expected)
|
||||||
|
|
||||||
|
def test_split_nodes_link_when_given_link_it_should_return_expected_list(self):
|
||||||
|
expected = [
|
||||||
|
TextNode("This is text with a link ", TextType.PLAIN),
|
||||||
|
TextNode("to boot dev", TextType.LINK, "https://www.boot.dev"),
|
||||||
|
TextNode(" and ", TextType.PLAIN),
|
||||||
|
TextNode(
|
||||||
|
"to youtube", TextType.LINK, "https://www.youtube.com/@bootdotdev"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
node = TextNode(
|
||||||
|
"This is text with a link [to boot dev](https://www.boot.dev) and [to youtube](https://www.youtube.com/@bootdotdev)",
|
||||||
|
TextType.PLAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = split_nodes_link([node])
|
||||||
|
|
||||||
|
self.assertListEqual(result, expected)
|
||||||
|
|
||||||
|
def test_text_to_text_node_when_called_it_should_return_expected_list(self):
|
||||||
|
expected = [
|
||||||
|
TextNode("This is ", TextType.PLAIN),
|
||||||
|
TextNode("text", TextType.BOLD),
|
||||||
|
TextNode(" with an ", TextType.PLAIN),
|
||||||
|
TextNode("italic", TextType.ITALIC),
|
||||||
|
TextNode(" word and a ", TextType.PLAIN),
|
||||||
|
TextNode("code block", TextType.CODE),
|
||||||
|
TextNode(" and an ", TextType.PLAIN),
|
||||||
|
TextNode(
|
||||||
|
"obi wan image", TextType.IMAGE, "https://i.imgur.com/fJRm4Vk.jpeg"
|
||||||
|
),
|
||||||
|
TextNode(" and a ", TextType.PLAIN),
|
||||||
|
TextNode("link", TextType.LINK, "https://boot.dev"),
|
||||||
|
]
|
||||||
|
|
||||||
|
text = "This is **text** with an _italic_ word and a `code block` and an  and a [link](https://boot.dev)"
|
||||||
|
|
||||||
|
result = text_to_text_node(text)
|
||||||
|
|
||||||
|
self.maxDiff = None
|
||||||
|
self.assertListEqual(result, expected)
|
||||||
|
|
||||||
|
def test_markdown_to_bocks_when_called_with_md_it_should_return_expected_blocks(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
expected = [
|
||||||
|
"This is **bolded** paragraph",
|
||||||
|
"This is another paragraph with _italic_ text and `code` here\nThis is the same paragraph on a new line",
|
||||||
|
"- This is a list\n- with items",
|
||||||
|
]
|
||||||
|
|
||||||
|
md = """
|
||||||
|
This is **bolded** paragraph
|
||||||
|
|
||||||
|
This is another paragraph with _italic_ text and `code` here
|
||||||
|
This is the same paragraph on a new line
|
||||||
|
|
||||||
|
- This is a list
|
||||||
|
- with items
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = markdown_to_blocks(md)
|
||||||
|
|
||||||
|
self.assertListEqual(result, expected)
|
||||||
|
|
||||||
|
def test_paragraphs(self):
|
||||||
|
expected = "<div><p>This is <b>bolded</b> paragraph text in a p tag here</p><p>This is another paragraph with <i>italic</i> text and <code>code</code> here</p></div>"
|
||||||
|
|
||||||
|
md = """
|
||||||
|
This is **bolded** paragraph
|
||||||
|
text in a p
|
||||||
|
tag here
|
||||||
|
|
||||||
|
This is another paragraph with _italic_ text and `code` here
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
node = markdown_to_html_node(md)
|
||||||
|
html = node.to_html()
|
||||||
|
|
||||||
|
self.assertEqual(html, expected)
|
||||||
|
|
||||||
|
def test_codeblock(self):
|
||||||
|
expected = "<div><pre><code>This is text that _should_ remain\nthe **same** even with inline stuff\n</code></pre></div>"
|
||||||
|
|
||||||
|
md = """
|
||||||
|
```
|
||||||
|
This is text that _should_ remain
|
||||||
|
the **same** even with inline stuff
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
node = markdown_to_html_node(md)
|
||||||
|
html = node.to_html()
|
||||||
|
|
||||||
|
with open("output.txt", "w") as file:
|
||||||
|
file.write(expected)
|
||||||
|
file.write("\n\n")
|
||||||
|
file.write(html)
|
||||||
|
|
||||||
|
self.assertEqual(html, expected)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from leafnode import LeafNode
|
||||||
|
from textnode import TextNode, TextType, text_node_to_html_node
|
||||||
|
|
||||||
|
|
||||||
|
class TestTextNode(unittest.TestCase):
|
||||||
|
def test_eq(self):
|
||||||
|
node = TextNode("This is a text node", TextType.BOLD)
|
||||||
|
node2 = TextNode("This is a text node", TextType.BOLD)
|
||||||
|
self.assertEqual(node, node2)
|
||||||
|
|
||||||
|
def test_eq_when_type_is_different_it_should_not_be_equal(self):
|
||||||
|
node = TextNode("This is a text node", TextType.LINK)
|
||||||
|
node2 = TextNode("This is a text node", TextType.BOLD)
|
||||||
|
self.assertNotEqual(node, node2)
|
||||||
|
|
||||||
|
def test_eq_when_text_is_different_it_should_not_be_equal(self):
|
||||||
|
node = TextNode("This is different", TextType.BOLD)
|
||||||
|
node2 = TextNode("This is a text node", TextType.BOLD)
|
||||||
|
self.assertNotEqual(node, node2)
|
||||||
|
|
||||||
|
def test_eq_when_url_is_different_it_should_not_be_equal(self):
|
||||||
|
node = TextNode("This is different", TextType.BOLD, "https://boot.dev")
|
||||||
|
node2 = TextNode("This is a text node", TextType.BOLD)
|
||||||
|
self.assertNotEqual(node, node2)
|
||||||
|
|
||||||
|
def test_text_node_to_html_node_when_plain_it_should_return_correct_leaf(self):
|
||||||
|
plain_text = "plain text"
|
||||||
|
expected = LeafNode(None, plain_text)
|
||||||
|
node = TextNode(plain_text, TextType.PLAIN)
|
||||||
|
|
||||||
|
result = text_node_to_html_node(node)
|
||||||
|
|
||||||
|
self.assertEqual(result.tag, expected.tag)
|
||||||
|
self.assertEqual(result.value, expected.value)
|
||||||
|
|
||||||
|
def test_text_node_to_html_node_when_bold_it_should_return_correct_leaf(self):
|
||||||
|
bold_text = "bold text"
|
||||||
|
expected = LeafNode("b", bold_text)
|
||||||
|
node = TextNode(bold_text, TextType.BOLD)
|
||||||
|
|
||||||
|
result = text_node_to_html_node(node)
|
||||||
|
|
||||||
|
self.assertEqual(result.tag, expected.tag)
|
||||||
|
self.assertEqual(result.value, expected.value)
|
||||||
|
|
||||||
|
def test_text_node_to_html_node_when_italics_it_should_return_correct_leaf(self):
|
||||||
|
italic_text = "italic text"
|
||||||
|
expected = LeafNode("i", italic_text)
|
||||||
|
node = TextNode(italic_text, TextType.ITALIC)
|
||||||
|
|
||||||
|
result = text_node_to_html_node(node)
|
||||||
|
|
||||||
|
self.assertEqual(result.tag, expected.tag)
|
||||||
|
self.assertEqual(result.value, expected.value)
|
||||||
|
|
||||||
|
def test_text_node_to_html_node_when_code_it_should_return_correct_leaf(self):
|
||||||
|
code_text = "var a = b;"
|
||||||
|
expected = LeafNode("code", code_text)
|
||||||
|
node = TextNode(code_text, TextType.CODE)
|
||||||
|
|
||||||
|
result = text_node_to_html_node(node)
|
||||||
|
|
||||||
|
self.assertEqual(result.tag, expected.tag)
|
||||||
|
self.assertEqual(result.value, expected.value)
|
||||||
|
|
||||||
|
def test_text_node_to_html_node_when_link_it_should_return_correct_leaf(self):
|
||||||
|
link_text = "click here"
|
||||||
|
link_url = "https://stevanfreeborn.com"
|
||||||
|
link_props = {"href": link_url}
|
||||||
|
expected = LeafNode("a", link_text, link_props)
|
||||||
|
node = TextNode(link_text, TextType.LINK, link_url)
|
||||||
|
|
||||||
|
result = text_node_to_html_node(node)
|
||||||
|
|
||||||
|
self.assertEqual(result.tag, expected.tag)
|
||||||
|
self.assertEqual(result.value, expected.value)
|
||||||
|
self.assertEqual(result.props, expected.props)
|
||||||
|
|
||||||
|
def test_text_node_to_html_node_when_image_it_should_return_correct_leaf(self):
|
||||||
|
image_alt_txt = "click here"
|
||||||
|
image_url = "https://stevanfreeborn.com/image.png"
|
||||||
|
image_props = {"src": image_url, "alt": image_alt_txt}
|
||||||
|
expected = LeafNode("img", "", image_props)
|
||||||
|
node = TextNode(image_alt_txt, TextType.IMAGE, image_url)
|
||||||
|
|
||||||
|
result = text_node_to_html_node(node)
|
||||||
|
|
||||||
|
self.assertEqual(result.tag, expected.tag)
|
||||||
|
self.assertEqual(result.value, expected.value)
|
||||||
|
self.assertEqual(result.props, expected.props)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from leafnode import LeafNode
|
||||||
|
|
||||||
|
|
||||||
|
class TextType(Enum):
|
||||||
|
PLAIN = "plain"
|
||||||
|
BOLD = "bold"
|
||||||
|
ITALIC = "italic"
|
||||||
|
CODE = "code"
|
||||||
|
LINK = "link"
|
||||||
|
IMAGE = "image"
|
||||||
|
|
||||||
|
|
||||||
|
class TextNode:
|
||||||
|
def __init__(self, text: str, text_type: TextType, url: str | None = None) -> None:
|
||||||
|
self.text = text
|
||||||
|
self.text_type = text_type
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, TextNode):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return (
|
||||||
|
self.text == other.text
|
||||||
|
and self.text_type == other.text_type
|
||||||
|
and self.url == other.url
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{TextNode.__name__}({self.text}, {self.text_type.value}, {self.url})"
|
||||||
|
|
||||||
|
|
||||||
|
def text_node_to_html_node(text_node: TextNode) -> LeafNode:
|
||||||
|
match text_node.text_type:
|
||||||
|
case TextType.PLAIN:
|
||||||
|
return LeafNode(None, text_node.text)
|
||||||
|
case TextType.BOLD:
|
||||||
|
return LeafNode("b", text_node.text)
|
||||||
|
case TextType.ITALIC:
|
||||||
|
return LeafNode("i", text_node.text)
|
||||||
|
case TextType.CODE:
|
||||||
|
return LeafNode("code", text_node.text)
|
||||||
|
case TextType.LINK:
|
||||||
|
return LeafNode("a", text_node.text, {"href": text_node.url})
|
||||||
|
case TextType.IMAGE:
|
||||||
|
return LeafNode("img", "", {"src": text_node.url, "alt": text_node.text})
|
||||||
|
case _:
|
||||||
|
raise ValueError
|
||||||
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 663 KiB |
|
After Width: | Height: | Size: 500 KiB |
@@ -0,0 +1,130 @@
|
|||||||
|
body {
|
||||||
|
background-color: #1f1c25;
|
||||||
|
color: #f0e6d1;
|
||||||
|
font-family: "Luminari", "Georgia", serif;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
color: #dda15e;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-shadow: 2px 2px 4px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #e0a96d;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 2px solid #e0a96d;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #f4a261;
|
||||||
|
border-color: #f4a261;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: #3c3c42;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #e9c46a;
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #3c3c42;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1em;
|
||||||
|
overflow: auto;
|
||||||
|
box-shadow: 2px 2px 6px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
background-color: #2e2c35;
|
||||||
|
border-left: 4px solid #8d99ae;
|
||||||
|
padding-left: 2em;
|
||||||
|
margin-left: 0;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
color: #ddd;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 3px solid #3c3c42;
|
||||||
|
box-shadow: 3px 3px 6px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #1f1c25;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #3c3c42;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 3px solid #1f1c25;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #5a5466;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #3c3c42 #1f1c25;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: #1f1c25;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{{ Title }}</title>
|
||||||
|
<link href="/index.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<article>{{ Content }}</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||