Add code snippets to long-form stories.
This commit is contained in:
@@ -17,6 +17,16 @@ Posts can include one JPEG, PNG, or WebP image up to 8 MB. Images are converted
|
|||||||
|
|
||||||
Long posts can also include multiple ordered step images. Upload them under **Step images**, then place `[[image:1]]`, `[[image:2]]`, and so on directly in the article text where each repair step needs an image. Each step image supports an optional caption and opens full-size when selected.
|
Long posts can also include multiple ordered step images. Upload them under **Step images**, then place `[[image:1]]`, `[[image:2]]`, and so on directly in the article text where each repair step needs an image. Each step image supports an optional caption and opens full-size when selected.
|
||||||
|
|
||||||
|
To add a code snippet, place it between triple-backtick fences. Add an optional language name after the opening fence to label the snippet:
|
||||||
|
|
||||||
|
````text
|
||||||
|
```python
|
||||||
|
print("hello, circuit")
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
Code is displayed as literal text and can be copied from the article view. Image markers written inside a code fence remain literal text.
|
||||||
|
|
||||||
## Accounts and MFA
|
## Accounts and MFA
|
||||||
|
|
||||||
The initial administrator account is `admin` with password `admin`, as requested for first-run access. Sign in, scan the displayed QR code with any iPhone or Android TOTP authenticator, and change this password before exposing the service to the internet. New registrations are held for approval in **Accounts**; accepted users must enroll a TOTP authenticator before they can publish.
|
The initial administrator account is `admin` with password `admin`, as requested for first-run access. Sign in, scan the displayed QR code with any iPhone or Android TOTP authenticator, and change this password before exposing the service to the internet. New registrations are held for approval in **Accounts**; accepted users must enroll a TOTP authenticator before they can publish.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
- [x] Configure TLS reverse proxy and production domain for `eternityproject.fi`.
|
- [x] Configure TLS reverse proxy and production domain for `eternityproject.fi`.
|
||||||
- [x] Add validated post image uploads with an in-article preview and click-to-expand view.
|
- [x] Add validated post image uploads with an in-article preview and click-to-expand view.
|
||||||
- [x] Add ordered inline step images with captions for long-form repair and engineering posts.
|
- [x] Add ordered inline step images with captions for long-form repair and engineering posts.
|
||||||
- [ ] Add code snippets to long-form stories.
|
- [x] Add code snippets to long-form stories.
|
||||||
- [x] Allow users to change their email addresses.
|
- [x] Allow users to change their email addresses.
|
||||||
- [x] Add user email verification for registration and email-address changes.
|
- [x] Add user email verification for registration and email-address changes.
|
||||||
- [x] Add user real names and use them as the published-by name.
|
- [x] Add user real names and use them as the published-by name.
|
||||||
|
|||||||
@@ -233,12 +233,18 @@ def save_inline_images(post_id):
|
|||||||
def article_blocks(body, images):
|
def article_blocks(body, images):
|
||||||
image_map = {str(index): image for index, image in enumerate(images, start=1)}
|
image_map = {str(index): image for index, image in enumerate(images, start=1)}
|
||||||
blocks = []
|
blocks = []
|
||||||
marker = re.compile(r"\[\[image:(\d+)\]\]")
|
marker = re.compile(
|
||||||
|
r"```(?P<language>[A-Za-z0-9][A-Za-z0-9_-]*)?[ \t]*\r?\n(?P<code>.*?)(?:\r?\n)?```|\[\[image:(?P<image>\d+)\]\]",
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
cursor = 0
|
cursor = 0
|
||||||
for match in marker.finditer(body):
|
for match in marker.finditer(body):
|
||||||
if match.start() > cursor:
|
if match.start() > cursor:
|
||||||
blocks.append(("text", body[cursor:match.start()]))
|
blocks.append(("text", body[cursor:match.start()]))
|
||||||
image = image_map.get(match.group(1))
|
if match.group("code") is not None:
|
||||||
|
blocks.append(("code", {"language": match.group("language") or "text", "content": match.group("code")}))
|
||||||
|
else:
|
||||||
|
image = image_map.get(match.group("image"))
|
||||||
if image is not None:
|
if image is not None:
|
||||||
blocks.append(("image", image))
|
blocks.append(("image", image))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ nav { display:flex; align-items:center; gap:22px; font-family:"DM Mono", monospa
|
|||||||
.auth-layout { min-height:calc(100vh - 160px); padding:70px max(10vw, 24px); display:grid; grid-template-columns:1fr minmax(280px, 390px); gap:10vw; align-items:center; }.auth-copy h1 { font-size:clamp(45px, 6vw, 82px); line-height:.9; margin:15px 0; }.auth-copy p { max-width:380px; font-size:18px; line-height:1.5; color:var(--muted); }.auth-form, .editor-form { display:grid; gap:19px; }.auth-form { padding:27px; border:1px solid var(--ink); background:rgba(241,241,233,.8); }.auth-form label, .editor-form label { display:grid; gap:7px; font:11px "DM Mono", monospace; }input, textarea { width:100%; resize:vertical; background:#fbfbf6; border:1px solid var(--ink); border-radius:0; padding:12px; color:var(--ink); font:15px "Space Grotesk", sans-serif; }input:focus, textarea:focus { outline:3px solid var(--acid); outline-offset:1px; }.form-switch { font-size:13px; color:var(--muted); }.form-switch a { color:var(--ink); text-decoration:underline; }.editor-form { max-width:900px; }.two-col { display:grid; grid-template-columns:1fr 2fr; gap:19px; }
|
.auth-layout { min-height:calc(100vh - 160px); padding:70px max(10vw, 24px); display:grid; grid-template-columns:1fr minmax(280px, 390px); gap:10vw; align-items:center; }.auth-copy h1 { font-size:clamp(45px, 6vw, 82px); line-height:.9; margin:15px 0; }.auth-copy p { max-width:380px; font-size:18px; line-height:1.5; color:var(--muted); }.auth-form, .editor-form { display:grid; gap:19px; }.auth-form { padding:27px; border:1px solid var(--ink); background:rgba(241,241,233,.8); }.auth-form label, .editor-form label { display:grid; gap:7px; font:11px "DM Mono", monospace; }input, textarea { width:100%; resize:vertical; background:#fbfbf6; border:1px solid var(--ink); border-radius:0; padding:12px; color:var(--ink); font:15px "Space Grotesk", sans-serif; }input:focus, textarea:focus { outline:3px solid var(--acid); outline-offset:1px; }.form-switch { font-size:13px; color:var(--muted); }.form-switch a { color:var(--ink); text-decoration:underline; }.editor-form { max-width:900px; }.two-col { display:grid; grid-template-columns:1fr 2fr; gap:19px; }
|
||||||
.step-images { margin:0; padding:16px; border:1px solid var(--ink); }.step-images legend { padding:0 5px; font:11px "DM Mono", monospace; }.step-images p { margin:0 0 15px; color:var(--muted); font-size:13px; line-height:1.45; }.step-images code { color:var(--orange); font:12px "DM Mono", monospace; }.step-image-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:12px; }.add-step-image { justify-self:start; border:1px solid var(--ink); background:transparent; padding:8px 10px; font:11px "DM Mono", monospace; cursor:pointer; }.add-step-image:hover { background:var(--acid); }
|
.step-images { margin:0; padding:16px; border:1px solid var(--ink); }.step-images legend { padding:0 5px; font:11px "DM Mono", monospace; }.step-images p { margin:0 0 15px; color:var(--muted); font-size:13px; line-height:1.45; }.step-images code { color:var(--orange); font:12px "DM Mono", monospace; }.step-image-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:12px; }.add-step-image { justify-self:start; border:1px solid var(--ink); background:transparent; padding:8px 10px; font:11px "DM Mono", monospace; cursor:pointer; }.add-step-image:hover { background:var(--acid); }
|
||||||
.article { max-width:850px; margin:0 auto; padding:80px 24px 100px; }.article h1 { font-size:clamp(48px, 7vw, 90px); line-height:.93; margin:23px 0; }.article-lead { border-left:4px solid var(--orange); padding-left:18px; max-width:720px; font-size:23px; line-height:1.35; }.byline { padding:20px 0; border-top:1px solid var(--ink); border-bottom:1px solid var(--ink); margin:35px 0; }.article-image-button { display:block; max-width:680px; width:100%; margin:0 0 30px; padding:0; border:0; background:none; cursor:zoom-in; }.article-image { display:block; width:100%; max-height:430px; object-fit:cover; border:1px solid var(--ink); }.inline-image { max-width:680px; margin:28px 0; }.inline-image .article-image-button { margin:0; }.inline-image figcaption { padding-top:8px; color:var(--muted); font:12px "DM Mono", monospace; }.image-dialog { width:min(94vw, 1200px); max-width:none; padding:0; border:1px solid var(--ink); background:var(--ink); }.image-dialog::backdrop { background:rgba(17,33,43,.85); }.image-dialog img { display:block; width:100%; max-height:88vh; object-fit:contain; }.dialog-close { position:absolute; top:10px; right:10px; border:1px solid var(--paper); background:var(--ink); color:var(--paper); padding:8px 10px; font:12px "DM Mono", monospace; cursor:pointer; }.article-body { max-width:680px; font-size:18px; line-height:1.7; margin-bottom:35px; }.article-text { white-space:pre-wrap; }
|
.article { max-width:850px; margin:0 auto; padding:80px 24px 100px; }.article h1 { font-size:clamp(48px, 7vw, 90px); line-height:.93; margin:23px 0; }.article-lead { border-left:4px solid var(--orange); padding-left:18px; max-width:720px; font-size:23px; line-height:1.35; }.byline { padding:20px 0; border-top:1px solid var(--ink); border-bottom:1px solid var(--ink); margin:35px 0; }.article-image-button { display:block; max-width:680px; width:100%; margin:0 0 30px; padding:0; border:0; background:none; cursor:zoom-in; }.article-image { display:block; width:100%; max-height:430px; object-fit:cover; border:1px solid var(--ink); }.inline-image { max-width:680px; margin:28px 0; }.inline-image .article-image-button { margin:0; }.inline-image figcaption { padding-top:8px; color:var(--muted); font:12px "DM Mono", monospace; }.image-dialog { width:min(94vw, 1200px); max-width:none; padding:0; border:1px solid var(--ink); background:var(--ink); }.image-dialog::backdrop { background:rgba(17,33,43,.85); }.image-dialog img { display:block; width:100%; max-height:88vh; object-fit:contain; }.dialog-close { position:absolute; top:10px; right:10px; border:1px solid var(--paper); background:var(--ink); color:var(--paper); padding:8px 10px; font:12px "DM Mono", monospace; cursor:pointer; }.article-body { max-width:680px; font-size:18px; line-height:1.7; margin-bottom:35px; }.article-text { white-space:pre-wrap; }
|
||||||
|
.code-block { margin:28px 0; border:1px solid var(--ink); background:var(--ink); }.code-block-head { display:flex; align-items:center; justify-content:space-between; min-height:38px; padding:7px 10px 7px 14px; color:var(--paper); font:11px "DM Mono", monospace; letter-spacing:.7px; text-transform:uppercase; }.copy-code { border:1px solid var(--paper); background:transparent; color:var(--paper); padding:5px 8px; font:11px "DM Mono", monospace; cursor:pointer; }.copy-code:hover { border-color:var(--acid); background:var(--acid); color:var(--ink); }.code-block pre { margin:0; padding:18px; overflow:auto; background:#fbfbf6; color:var(--ink); font:14px/1.55 "DM Mono", monospace; white-space:pre; }.code-block code { font:inherit; }
|
||||||
.flash { margin:16px max(5vw, 24px) 0; padding:11px 14px; font:12px "DM Mono", monospace; border:1px solid var(--ink); }.flash.error { border-color:var(--orange); background:#ffe3d9; }.flash.success { background:var(--acid); }footer { border-top:1px solid var(--ink); padding:23px max(5vw, 24px); display:flex; justify-content:space-between; font:10px "DM Mono", monospace; color:var(--muted); }
|
.flash { margin:16px max(5vw, 24px) 0; padding:11px 14px; font:12px "DM Mono", monospace; border:1px solid var(--ink); }.flash.error { border-color:var(--orange); background:#ffe3d9; }.flash.success { background:var(--acid); }footer { border-top:1px solid var(--ink); padding:23px max(5vw, 24px); display:flex; justify-content:space-between; font:10px "DM Mono", monospace; color:var(--muted); }
|
||||||
.mfa-qr { display:block; width:200px; max-width:100%; margin-top:28px; border:8px solid #fff; image-rendering:pixelated; }.mfa-manual { margin-top:18px; color:var(--muted); font:12px "DM Mono", monospace; }.mfa-manual summary { cursor:pointer; }.mfa-secret { display:block; width:max-content; max-width:100%; overflow-wrap:anywhere; margin-top:10px; padding:14px; border:1px solid var(--ink); background:var(--acid); color:var(--ink); font:14px "DM Mono", monospace; }.account-table { border:1px solid var(--ink); }.account-row { display:grid; grid-template-columns:2fr 1.5fr 1fr 1fr; gap:16px; align-items:center; min-height:64px; padding:12px 16px; border-bottom:1px solid var(--ink); font:13px "DM Mono", monospace; }.account-row:last-child { border-bottom:0; }.account-head { min-height:auto; padding:10px 16px; background:var(--ink); color:var(--paper); font-size:10px; }.account-row small { color:var(--orange); font:10px "DM Mono", monospace; }.account-row .email { display:block; margin-top:5px; color:var(--muted); overflow-wrap:anywhere; }
|
.mfa-qr { display:block; width:200px; max-width:100%; margin-top:28px; border:8px solid #fff; image-rendering:pixelated; }.mfa-manual { margin-top:18px; color:var(--muted); font:12px "DM Mono", monospace; }.mfa-manual summary { cursor:pointer; }.mfa-secret { display:block; width:max-content; max-width:100%; overflow-wrap:anywhere; margin-top:10px; padding:14px; border:1px solid var(--ink); background:var(--acid); color:var(--ink); font:14px "DM Mono", monospace; }.account-table { border:1px solid var(--ink); }.account-row { display:grid; grid-template-columns:2fr 1.5fr 1fr 1fr; gap:16px; align-items:center; min-height:64px; padding:12px 16px; border-bottom:1px solid var(--ink); font:13px "DM Mono", monospace; }.account-row:last-child { border-bottom:0; }.account-head { min-height:auto; padding:10px 16px; background:var(--ink); color:var(--paper); font-size:10px; }.account-row small { color:var(--orange); font:10px "DM Mono", monospace; }.account-row .email { display:block; margin-top:5px; color:var(--muted); overflow-wrap:anywhere; }
|
||||||
@media (max-width:720px) { .site-header { align-items:flex-start; }.site-header nav { justify-content:flex-end; flex-wrap:wrap; gap:10px 15px; }.hero { min-height:560px; }.scope { width:220px; height:220px; right:-45px; }.post-grid, .auth-layout, .two-col, .step-image-row { grid-template-columns:1fr; }.auth-layout { gap:40px; padding:60px 24px; }footer { flex-wrap:wrap; gap:8px 16px; }.user-chip { display:none; }.account-row { grid-template-columns:1fr 1fr; }.account-head { display:none; } }
|
@media (max-width:720px) { .site-header { align-items:flex-start; }.site-header nav { justify-content:flex-end; flex-wrap:wrap; gap:10px 15px; }.hero { min-height:560px; }.scope { width:220px; height:220px; right:-45px; }.post-grid, .auth-layout, .two-col, .step-image-row { grid-template-columns:1fr; }.auth-layout { gap:40px; padding:60px 24px; }footer { flex-wrap:wrap; gap:8px 16px; }.user-chip { display:none; }.account-row { grid-template-columns:1fr 1fr; }.account-head { display:none; } }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<label>Title<input name="title" required value="{{ post.title if post else '' }}" placeholder="A clear, useful heading"></label>
|
<label>Title<input name="title" required value="{{ post.title if post else '' }}" placeholder="A clear, useful heading"></label>
|
||||||
<div class="two-col"><label>Category<input name="category" value="{{ post.category if post else 'Engineering' }}" placeholder="Engineering"></label><label>Summary<input name="excerpt" required value="{{ post.excerpt if post else '' }}" placeholder="The one-paragraph signal"></label></div>
|
<div class="two-col"><label>Category<input name="category" value="{{ post.category if post else 'Engineering' }}" placeholder="Engineering"></label><label>Summary<input name="excerpt" required value="{{ post.excerpt if post else '' }}" placeholder="The one-paragraph signal"></label></div>
|
||||||
<label>Article<textarea name="body" required rows="16" placeholder="Write in plain text. Paragraph breaks are preserved.">{{ post.body if post else '' }}</textarea></label>
|
<label>Article<textarea name="body" required rows="16" placeholder="Write in plain text. Paragraph breaks are preserved.">{{ post.body if post else '' }}</textarea><small>Wrap code in triple backticks. Add a language after the opening backticks, for example <code>```python</code>.</small></label>
|
||||||
<label>Article image<input name="image" type="file" accept="image/jpeg,image/png,image/webp">{% if post and post.image_filename %}<small>Leave empty to retain the current image.</small>{% endif %}</label>
|
<label>Article image<input name="image" type="file" accept="image/jpeg,image/png,image/webp">{% if post and post.image_filename %}<small>Leave empty to retain the current image.</small>{% endif %}</label>
|
||||||
<fieldset class="step-images"><legend>Step images</legend><p>Upload images for individual repair steps, then place <code>[[image:1]]</code>, <code>[[image:2]]</code>, and so on in the article text.</p><div id="step-image-list"><div class="step-image-row"><label>Image<input name="images" type="file" accept="image/jpeg,image/png,image/webp"></label><label>Caption<input name="image_captions" type="text" placeholder="Optional caption"></label></div></div><button class="add-step-image" type="button">Add another step image</button></fieldset>
|
<fieldset class="step-images"><legend>Step images</legend><p>Upload images for individual repair steps, then place <code>[[image:1]]</code>, <code>[[image:2]]</code>, and so on in the article text.</p><div id="step-image-list"><div class="step-image-row"><label>Image<input name="images" type="file" accept="image/jpeg,image/png,image/webp"></label><label>Caption<input name="image_captions" type="text" placeholder="Optional caption"></label></div></div><button class="add-step-image" type="button">Add another step image</button></fieldset>
|
||||||
<button class="button" type="submit">Publish transmission <span aria-hidden="true">→</span></button>
|
<button class="button" type="submit">Publish transmission <span aria-hidden="true">→</span></button>
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}{{ post.title }} | Eternity Project{% endblock %}
|
{% block title %}{{ post.title }} | Eternity Project{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<article class="article"><div class="post-meta"><span>{{ post.category }}</span><time datetime="{{ post.created_at }}">{{ post.created_at[:10] }}</time></div><h1>{{ post.title }}</h1><p class="article-lead">{{ post.excerpt }}</p><div class="byline">WRITTEN BY {{ post.real_name or post.username }}{% if post.updated_at != post.created_at %} / UPDATED {{ post.updated_at[:10] }}{% endif %}</div>{% if post.image_filename %}<button class="article-image-button" type="button" data-image="{{ url_for('uploaded_image', filename=post.image_filename) }}" data-alt="Image for {{ post.title }}"><img class="article-image" src="{{ url_for('uploaded_image', filename=post.image_filename) }}" alt="Image for {{ post.title }}"></button>{% endif %}<div class="article-body">{% for kind, content in blocks %}{% if kind == 'text' %}<div class="article-text">{{ content }}</div>{% else %}<figure class="inline-image"><button class="article-image-button" type="button" data-image="{{ url_for('uploaded_image', filename=content.filename) }}" data-alt="{{ content.caption or 'Article step image' }}"><img class="article-image" src="{{ url_for('uploaded_image', filename=content.filename) }}" alt="{{ content.caption or 'Article step image' }}"></button>{% if content.caption %}<figcaption>{{ content.caption }}</figcaption>{% endif %}</figure>{% endif %}{% endfor %}</div>{% if current_user and current_user.id == post.author_id %}<a class="button compact" href="{{ url_for('edit_post', slug=post.slug) }}">Edit post</a>{% endif %}</article>
|
<article class="article"><div class="post-meta"><span>{{ post.category }}</span><time datetime="{{ post.created_at }}">{{ post.created_at[:10] }}</time></div><h1>{{ post.title }}</h1><p class="article-lead">{{ post.excerpt }}</p><div class="byline">WRITTEN BY {{ post.real_name or post.username }}{% if post.updated_at != post.created_at %} / UPDATED {{ post.updated_at[:10] }}{% endif %}</div>{% if post.image_filename %}<button class="article-image-button" type="button" data-image="{{ url_for('uploaded_image', filename=post.image_filename) }}" data-alt="Image for {{ post.title }}"><img class="article-image" src="{{ url_for('uploaded_image', filename=post.image_filename) }}" alt="Image for {{ post.title }}"></button>{% endif %}<div class="article-body">{% for kind, content in blocks %}{% if kind == 'text' %}<div class="article-text">{{ content }}</div>{% elif kind == 'code' %}<section class="code-block"><div class="code-block-head"><span>{{ content.language }}</span><button class="copy-code" type="button">Copy</button></div><pre><code class="language-{{ content.language }}">{{ content.content }}</code></pre></section>{% else %}<figure class="inline-image"><button class="article-image-button" type="button" data-image="{{ url_for('uploaded_image', filename=content.filename) }}" data-alt="{{ content.caption or 'Article step image' }}"><img class="article-image" src="{{ url_for('uploaded_image', filename=content.filename) }}" alt="{{ content.caption or 'Article step image' }}"></button>{% if content.caption %}<figcaption>{{ content.caption }}</figcaption>{% endif %}</figure>{% endif %}{% endfor %}</div>{% if current_user and current_user.id == post.author_id %}<a class="button compact" href="{{ url_for('edit_post', slug=post.slug) }}">Edit post</a>{% endif %}</article>
|
||||||
<dialog class="image-dialog"><button class="dialog-close" type="button" aria-label="Close full-size image">Close</button><img alt=""></dialog>
|
<dialog class="image-dialog"><button class="dialog-close" type="button" aria-label="Close full-size image">Close</button><img alt=""></dialog>
|
||||||
<script>const imageDialog=document.querySelector('.image-dialog'),dialogImage=imageDialog.querySelector('img');document.querySelectorAll('.article-image-button').forEach(button=>button.addEventListener('click',()=>{dialogImage.src=button.dataset.image;dialogImage.alt=button.dataset.alt;imageDialog.showModal()}));imageDialog.querySelector('.dialog-close').addEventListener('click',()=>imageDialog.close());imageDialog.addEventListener('click',event=>{if(event.target===imageDialog)imageDialog.close()});</script>
|
<script>const imageDialog=document.querySelector('.image-dialog'),dialogImage=imageDialog.querySelector('img');document.querySelectorAll('.article-image-button').forEach(button=>button.addEventListener('click',()=>{dialogImage.src=button.dataset.image;dialogImage.alt=button.dataset.alt;imageDialog.showModal()}));imageDialog.querySelector('.dialog-close').addEventListener('click',()=>imageDialog.close());imageDialog.addEventListener('click',event=>{if(event.target===imageDialog)imageDialog.close()});document.querySelectorAll('.copy-code').forEach(button=>button.addEventListener('click',async()=>{await navigator.clipboard.writeText(button.closest('.code-block').querySelector('code').textContent);button.textContent='Copied';setTimeout(()=>button.textContent='Copy',1500)}));</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import app as application
|
||||||
|
|
||||||
|
|
||||||
|
class CodeSnippetTestCase(unittest.TestCase):
|
||||||
|
def test_fenced_code_is_parsed_with_its_language(self):
|
||||||
|
blocks = application.article_blocks('Intro\n```python\nprint("hi")\n```\nEnd', [])
|
||||||
|
|
||||||
|
self.assertEqual(blocks[0], ("text", "Intro\n"))
|
||||||
|
self.assertEqual(blocks[1], ("code", {"language": "python", "content": 'print("hi")'}))
|
||||||
|
self.assertEqual(blocks[2], ("text", "\nEnd"))
|
||||||
|
|
||||||
|
def test_code_fence_keeps_image_marker_as_literal_code(self):
|
||||||
|
image = {"filename": "step.webp"}
|
||||||
|
blocks = application.article_blocks("```\n[[image:1]]\n```\n[[image:1]]", [image])
|
||||||
|
|
||||||
|
self.assertEqual(blocks[0], ("code", {"language": "text", "content": "[[image:1]]"}))
|
||||||
|
self.assertEqual(blocks[1], ("text", "\n"))
|
||||||
|
self.assertEqual(blocks[2], ("image", image))
|
||||||
|
|
||||||
|
def test_unterminated_fence_is_plain_text(self):
|
||||||
|
body = "```python\nprint('not closed')"
|
||||||
|
|
||||||
|
self.assertEqual(application.article_blocks(body, []), [("text", body)])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user