Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimenting with blockquotes #853

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {CachedObjectsProvider} from '~/hooks/useCachedObjects'
import {useTheme} from '~/hooks/theme'
import {loadQuestionDetail} from '~/server-utils/stampy'
import GlobalBanners from './components/GlobalBanners'
import markdownStyles from '~/styles/markdown.css'

/*
* Transform the given text into a meta header format.
Expand Down Expand Up @@ -84,7 +85,7 @@ export const meta: MetaFunction<typeof loader> = ({data = {} as any}) => {
}

export const links: LinksFunction = () =>
[newStyles, cssBundleHref]
[newStyles, cssBundleHref, markdownStyles]
.filter((i) => i)
.map((styles) => ({rel: 'stylesheet', href: styles as string}))

Expand Down
129 changes: 129 additions & 0 deletions app/routes/markdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React, { useState, useEffect } from 'react';

// QuestionContent component included in the same file
const QuestionContent = ({ question }) => {
// Convert markdown-style blockquotes to HTML blockquotes
const processMarkdown = (markdown) => {
if (!markdown) return '';

// Split into lines to process blockquotes
const lines = markdown.split('\n');
let inBlockquote = false;
let processedLines = [];
let currentBlockquote = [];

for (let line of lines) {
// Check if line starts with blockquote marker
if (line.trim().startsWith('>')) {
// Remove the '>' and trim
const content = line.replace(/^>/, '').trim();
if (!inBlockquote) {
inBlockquote = true;
currentBlockquote = [];
}
currentBlockquote.push(content);
} else {
// If we were in a blockquote, close it
if (inBlockquote) {
processedLines.push(`<blockquote class="stampy-blockquote">${currentBlockquote.join('<br/>')}</blockquote>`);
inBlockquote = false;
currentBlockquote = [];
}
// Keep non-blockquote lines as-is
if (line.trim()) {
processedLines.push(`<p>${line}</p>`);
}
}
}

// Close any remaining blockquote
if (inBlockquote) {
processedLines.push(`<blockquote class="stampy-blockquote">${currentBlockquote.join('<br/>')}</blockquote>`);
}

return processedLines.join('\n');
};

// Process the markdown and return HTML
const processContent = () => {
const content = question?.markdown || question?.text;
if (!content) return '';
return processMarkdown(content);
};

return processContent();
};

const defaultContent = `> I'm envisioning that in the future there will also be systems where you can input any conclusion that you want to argue (including moral conclusions) and the target audience, and the system will give you the most convincing arguments for it. At that point people won't be able to participate in any online (or offline for that matter) discussions without risking their object-level values being hijacked.

This is regular text with a [link to example](https://example.com).

> This is another blockquote to test styling.
> With multiple lines.

Regular paragraph with **bold** and *italic* text.`;

// Function to safely get question markdown from window context
const getQuestionMarkdown = () => {
try {
if (typeof window !== 'undefined') {
return window?.__remixContext?.state?.loaderData?.root?.question?.markdown;
}
return null;
} catch (e) {
return null;
}
};

const MarkdownTest = () => {
const [markdown, setMarkdown] = useState(defaultContent);
const [questionMarkdown, setQuestionMarkdown] = useState(null);

// Use useEffect to safely access window after component mounts
useEffect(() => {
const qMarkdown = getQuestionMarkdown();
if (qMarkdown) {
setQuestionMarkdown(qMarkdown);
}
}, []);

return (
<div className="page">
<div className="page-body padding-top-80">
<h1 className="padding-bottom-40">Markdown Test Page</h1>

<div className="section-grid">
<div>
<h2 className="padding-bottom-16">Input Markdown</h2>
<textarea
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
className="w-full h-64 p-4 border rounded font-mono text-sm"
/>
</div>

<div>
<h2 className="padding-bottom-16">Rendered Output</h2>
<div
className="prose max-w-none p-4 border rounded"
dangerouslySetInnerHTML={{
__html: QuestionContent({ question: { markdown }})
}}
/>
</div>

{questionMarkdown && (
<div>
<h2 className="padding-bottom-16">Raw Markdown from Question</h2>
<pre className="p-4 border rounded bg-gray-50 overflow-auto">
{JSON.stringify(questionMarkdown, null, 2)}
</pre>
</div>
)}
</div>
</div>
</div>
);
};

export default MarkdownTest;
65 changes: 54 additions & 11 deletions app/server-utils/parsing-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,60 @@
import MarkdownIt from 'markdown-it'
import MarkdownItFootnote from 'markdown-it-footnote'


const md = new MarkdownIt({
html: true,
typographer: true,
quotes: '""\'\'',
breaks: true,
}).use(MarkdownItFootnote)

// Force blockquotes to be preserved
md.enable('blockquote')

// Add explicit rendering rules for blockquotes
md.renderer.rules.blockquote_open = () => '<blockquote class="stampy-blockquote">'
md.renderer.rules.blockquote_close = () => '</blockquote>\n'

// When processing text inside blockquotes, preserve line breaks
md.renderer.rules.text = (tokens, idx) => {
if (tokens[idx].content.includes('\n')) {
return tokens[idx].content.split('\n').join('<br/>\n')
}
return tokens[idx].content
}

// Update CSS to match
// Add paragraph styling within blockquotes
md.renderer.rules.paragraph_open = (tokens, idx) => {
const token = tokens[idx]
if (token.block && token.level > 0) { // Check if we're inside a blockquote
return '<p class="m-0">'
}
return '<p>'
}

md.renderer.rules.footnote_caption = (tokens, idx) => {
let n = Number(tokens[idx].meta.id + 1).toString()
if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`
return n
}

export const convertMarkdownToHtml = (markdown: string, pageid?: string): string => {
// First convert markdown to HTML
let html = md.render(markdown)

// Apply post-processing
html = cleanUpDoubleBold(html)
html = allLinksOnNewTab(html)

if (pageid) {
html = uniqueFootnotes(html, pageid)
}

return html
}

/**
* Replaces all whitelisted URLs in the text with iframes of the URLs
* @param markdown text in which URLs should be replaced
Expand Down Expand Up @@ -41,17 +95,6 @@ interface HostConfig {
sandboxValue: string
}

const md = new MarkdownIt({html: true}).use(MarkdownItFootnote)
md.renderer.rules.footnote_caption = (tokens, idx) => {
let n = Number(tokens[idx].meta.id + 1).toString()
if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`
return n
}

export const convertMarkdownToHtml = (markdown: string): string => {
return md.render(markdown)
}

export const uniqueFootnotes = (html: string, pageid: string): string => {
// Make sure the footnotes point to unique ids. This is very ugly and would be better handled
// with a proper parser, but should do the trick so is fine? Maybe?
Expand Down
35 changes: 35 additions & 0 deletions app/styles/markdown.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* markdown.css */

.stampy-blockquote {
/* Force block display and ensure it takes width */
display: block !important;
width: 100% !important;

/* Styling */
border-left: 4px solid var(--colors-teal-500) !important;
background: var(--colors-teal-50) !important;
padding: 1rem 2rem !important;
margin: 1.5rem 0 !important;

/* Text */
color: var(--colors-cool-grey-800) !important;

/* Shape */
border-radius: 0 0.25rem 0.25rem 0 !important;
}

/* Handle nested content */
.stampy-blockquote p {
margin: 0 !important;
}

.stampy-blockquote p + p {
margin-top: 1rem !important;
}

/* Force styles in any context */
.contents .article-content blockquote,
.article-container .contents blockquote,
blockquote.stampy-blockquote {
composes: stampy-blockquote;
}
Loading