Everywhere: Remove LibMarkdown

This was used to convert markdown into HTML for display in the browser,
but no other browser behaves this way, so let's simplify things by
removing it.

(Yes, we could implement all kinds of "convert to HTML and display" for
every file format out there, but that's far outside the scope of a
browser engine.)
This commit is contained in:
Andreas Kling 2024-06-04 07:52:10 +02:00
parent edb527e04d
commit e4cd91761d
Notes: sideshowbarker 2024-07-18 04:46:35 +09:00
43 changed files with 1 additions and 3148 deletions

View File

@ -437,7 +437,6 @@ if (BUILD_LAGOM)
JS
Line
Locale
Markdown
Protocol
Regex
RIFF
@ -562,7 +561,6 @@ if (BUILD_LAGOM)
LibCompress
LibGfx
LibLocale
LibMarkdown
LibSQL
LibTest
LibTextCodec

View File

@ -1,19 +0,0 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/OwnPtr.h>
#include <AK/StringView.h>
#include <LibMarkdown/Document.h>
#include <stddef.h>
#include <stdint.h>
extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size)
{
AK::set_debug_enabled(false);
auto markdown = StringView(static_cast<unsigned char const*>(data), size);
(void)Markdown::Document::parse(markdown);
return 0;
}

View File

@ -23,7 +23,6 @@ set(FUZZER_TARGETS
JsonParser
LzmaDecompression
LzmaRoundtrip
Markdown
MatroskaReader
MD5
MP3Loader
@ -91,7 +90,6 @@ set(FUZZER_DEPENDENCIES_JPEGLoader LibGfx)
set(FUZZER_DEPENDENCIES_Js LibJS)
set(FUZZER_DEPENDENCIES_LzmaDecompression LibArchive LibCompress)
set(FUZZER_DEPENDENCIES_LzmaRoundtrip LibCompress)
set(FUZZER_DEPENDENCIES_Markdown LibMarkdown)
set(FUZZER_DEPENDENCIES_MatroskaReader LibVideo)
set(FUZZER_DEPENDENCIES_MD5 LibCrypto)
set(FUZZER_DEPENDENCIES_MP3Loader LibAudio)

View File

@ -392,7 +392,6 @@ if (current_os != "mac") {
"//Userland/Libraries/LibImageDecoderClient",
"//Userland/Libraries/LibJS",
"//Userland/Libraries/LibLine",
"//Userland/Libraries/LibMarkdown",
"//Userland/Libraries/LibProtocol",
"//Userland/Libraries/LibRIFF",
"//Userland/Libraries/LibRegex",
@ -427,7 +426,6 @@ if (current_os != "mac") {
"$root_out_dir/lib/liblagom-ipc.dylib",
"$root_out_dir/lib/liblagom-js.dylib",
"$root_out_dir/lib/liblagom-line.dylib",
"$root_out_dir/lib/liblagom-markdown.dylib",
"$root_out_dir/lib/liblagom-protocol.dylib",
"$root_out_dir/lib/liblagom-regex.dylib",
"$root_out_dir/lib/liblagom-riff.dylib",

View File

@ -1,24 +0,0 @@
shared_library("LibMarkdown") {
output_name = "markdown"
include_dirs = [ "//Userland/Libraries" ]
sources = [
"BlockQuote.cpp",
"CodeBlock.cpp",
"CommentBlock.cpp",
"ContainerBlock.cpp",
"Document.cpp",
"Heading.cpp",
"HorizontalRule.cpp",
"LineIterator.cpp",
"List.cpp",
"Paragraph.cpp",
"Table.cpp",
"Text.cpp",
]
deps = [
"//AK",
"//Userland/Libraries/LibCore",
"//Userland/Libraries/LibJS",
"//Userland/Libraries/LibRegex",
]
}

View File

@ -371,7 +371,6 @@ shared_library("LibWeb") {
"//Userland/Libraries/LibIPC",
"//Userland/Libraries/LibJS",
"//Userland/Libraries/LibLocale",
"//Userland/Libraries/LibMarkdown",
"//Userland/Libraries/LibRegex",
"//Userland/Libraries/LibSyntax",
"//Userland/Libraries/LibTLS",

View File

@ -6,7 +6,6 @@ add_subdirectory(LibDiff)
add_subdirectory(LibGfx)
add_subdirectory(LibJS)
add_subdirectory(LibLocale)
add_subdirectory(LibMarkdown)
add_subdirectory(LibRegex)
add_subdirectory(LibSQL)
add_subdirectory(LibTest)

View File

@ -1,14 +0,0 @@
include(commonmark_spec)
set(TEST_SOURCES
TestCommonmark.cpp
TestImageSizeExtension.cpp
)
foreach(source IN LISTS TEST_SOURCES)
serenity_test("${source}" LibMarkdown LIBS LibMarkdown)
endforeach()
if (BUILD_LAGOM)
set_tests_properties(TestCommonmark PROPERTIES DISABLED YES)
endif()

View File

@ -1,48 +0,0 @@
/*
* Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteString.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/JsonParser.h>
#include <LibCore/File.h>
#include <LibMarkdown/Document.h>
#include <LibTest/TestCase.h>
#include <LibTest/TestSuite.h>
TEST_SETUP
{
auto file_or_error = Core::File::open("/home/anon/Tests/commonmark.spec.json"sv, Core::File::OpenMode::Read);
if (file_or_error.is_error())
file_or_error = Core::File::open("./commonmark.spec.json"sv, Core::File::OpenMode::Read);
VERIFY(!file_or_error.is_error());
auto file = file_or_error.release_value();
auto file_size = MUST(file->size());
auto content = MUST(ByteBuffer::create_uninitialized(file_size));
MUST(file->read_until_filled(content.bytes()));
ByteString test_data { content.bytes() };
auto tests = JsonParser(test_data).parse().value().as_array();
for (size_t i = 0; i < tests.size(); ++i) {
auto testcase = tests[i].as_object();
auto name = ByteString::formatted("{}_ex{}_{}..{}",
testcase.get("section"sv).value(),
testcase.get("example"sv).value(),
testcase.get("start_line"sv).value(),
testcase.get("end_line"sv).value());
ByteString markdown = testcase.get_byte_string("markdown"sv).value();
ByteString html = testcase.get_byte_string("html"sv).value();
Test::TestSuite::the().add_case(adopt_ref(*new Test::TestCase(
name, [markdown, html]() {
auto document = Markdown::Document::parse(markdown);
EXPECT_EQ(document->render_to_inline_html(), html);
},
false)));
}
}

View File

@ -1,39 +0,0 @@
/*
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Array.h>
#include <LibMarkdown/Document.h>
#include <LibTest/TestCase.h>
struct TestCase {
StringView markdown;
StringView expected_html;
};
static constexpr Array image_size_tests {
// No image size:
TestCase { .markdown = "![](foo.png)"sv, .expected_html = R"(<p><img src="foo.png" alt="" ></p>)"sv },
// Only width given:
TestCase { .markdown = "![](foo.png =100x)"sv, .expected_html = R"(<p><img src="foo.png" style="width: 100px;" alt="" ></p>)"sv },
// Only height given:
TestCase { .markdown = "![](foo.png =x200)"sv, .expected_html = R"(<p><img src="foo.png" style="height: 200px;" alt="" ></p>)"sv },
// Both width and height given
TestCase { .markdown = "![](foo.png =50x25)"sv, .expected_html = R"(<p><img src="foo.png" style="width: 50px;height: 25px;" alt="" ></p>)"sv },
// Size contains invalid width
TestCase { .markdown = "![](foo.png =1oox50)"sv, .expected_html = R"(<p><img src="foo.png =1oox50" alt="" ></p>)"sv },
// Size contains invalid height
TestCase { .markdown = "![](foo.png =900xfour)"sv, .expected_html = R"(<p><img src="foo.png =900xfour" alt="" ></p>)"sv },
};
TEST_CASE(test_image_size_markdown_extension)
{
for (auto const& test_case : image_size_tests) {
auto document = Markdown::Document::parse(test_case.markdown);
auto raw_rendered_html = document->render_to_inline_html();
auto rendered_html = StringView(raw_rendered_html).trim_whitespace();
EXPECT_EQ(rendered_html, test_case.expected_html);
}
}

View File

@ -22,7 +22,6 @@ add_subdirectory(LibKeyboard)
add_subdirectory(LibLine)
add_subdirectory(LibLocale)
add_subdirectory(LibMain)
add_subdirectory(LibMarkdown)
add_subdirectory(LibProtocol)
add_subdirectory(LibRegex)
add_subdirectory(LibRIFF)

View File

@ -1,26 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/RecursionDecision.h>
#include <AK/StringView.h>
#include <AK/Vector.h>
#include <LibMarkdown/Forward.h>
namespace Markdown {
class Block {
public:
virtual ~Block() = default;
virtual ByteString render_to_html(bool tight = false) const = 0;
virtual Vector<ByteString> render_lines_for_terminal(size_t view_width = 0) const = 0;
virtual RecursionDecision walk(Visitor&) const = 0;
};
}

View File

@ -1,59 +0,0 @@
/*
* Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringBuilder.h>
#include <AK/Vector.h>
#include <LibMarkdown/BlockQuote.h>
#include <LibMarkdown/Visitor.h>
namespace Markdown {
ByteString BlockQuote::render_to_html(bool) const
{
StringBuilder builder;
builder.append("<blockquote>\n"sv);
builder.append(m_contents->render_to_html());
builder.append("</blockquote>\n"sv);
return builder.to_byte_string();
}
Vector<ByteString> BlockQuote::render_lines_for_terminal(size_t view_width) const
{
Vector<ByteString> lines;
size_t child_width = view_width < 4 ? 0 : view_width - 4;
for (auto& line : m_contents->render_lines_for_terminal(child_width))
lines.append(ByteString::formatted(" {}", line));
return lines;
}
RecursionDecision BlockQuote::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
return m_contents->walk(visitor);
}
OwnPtr<BlockQuote> BlockQuote::parse(LineIterator& lines)
{
lines.push_context(LineIterator::Context::block_quote());
if (lines.is_end()) {
lines.pop_context();
return {};
}
auto contents = ContainerBlock::parse(lines);
lines.pop_context();
if (!contents)
return {};
return make<BlockQuote>(move(contents));
}
}

View File

@ -1,34 +0,0 @@
/*
* Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/OwnPtr.h>
#include <LibMarkdown/Block.h>
#include <LibMarkdown/ContainerBlock.h>
namespace Markdown {
class BlockQuote final : public Block {
public:
BlockQuote(OwnPtr<ContainerBlock> contents)
: m_contents(move(contents))
{
}
virtual ~BlockQuote() override = default;
virtual ByteString render_to_html(bool tight = false) const override;
virtual Vector<ByteString> render_lines_for_terminal(size_t view_width = 0) const override;
virtual RecursionDecision walk(Visitor&) const override;
static OwnPtr<BlockQuote> parse(LineIterator& lines);
private:
OwnPtr<ContainerBlock> m_contents;
};
}

View File

@ -1,18 +0,0 @@
set(SOURCES
BlockQuote.cpp
CodeBlock.cpp
CommentBlock.cpp
ContainerBlock.cpp
Document.cpp
Heading.cpp
HorizontalRule.cpp
LineIterator.cpp
List.cpp
Paragraph.cpp
SyntaxHighlighter.cpp
Table.cpp
Text.cpp
)
serenity_lib(LibMarkdown markdown)
target_link_libraries(LibMarkdown PRIVATE LibUnicode LibJS LibRegex LibSyntax)

View File

@ -1,201 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2022, Peter Elliott <pelliott@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Forward.h>
#include <AK/StringBuilder.h>
#include <LibJS/MarkupGenerator.h>
#include <LibMarkdown/CodeBlock.h>
#include <LibMarkdown/Visitor.h>
#include <LibRegex/Regex.h>
namespace Markdown {
ByteString CodeBlock::render_to_html(bool) const
{
StringBuilder builder;
builder.append("<pre>"sv);
if (m_style.length() >= 2)
builder.append("<strong>"sv);
else if (m_style.length() >= 2)
builder.append("<em>"sv);
if (m_language.is_empty())
builder.append("<code>"sv);
else
builder.appendff("<code class=\"language-{}\">", escape_html_entities(m_language));
if (m_language == "js") {
auto html_or_error = JS::MarkupGenerator::html_from_source(m_code);
if (html_or_error.is_error()) {
warnln("Could not render js code to html: {}", html_or_error.error());
builder.append(escape_html_entities(m_code));
} else {
builder.append(html_or_error.release_value());
}
} else {
builder.append(escape_html_entities(m_code));
}
builder.append("</code>"sv);
if (m_style.length() >= 2)
builder.append("</strong>"sv);
else if (m_style.length() >= 2)
builder.append("</em>"sv);
builder.append("</pre>\n"sv);
return builder.to_byte_string();
}
Vector<ByteString> CodeBlock::render_lines_for_terminal(size_t) const
{
Vector<ByteString> lines;
// Do not indent too much if we are in the synopsis
auto indentation = " "sv;
if (m_current_section != nullptr) {
auto current_section_name = m_current_section->render_lines_for_terminal()[0];
if (current_section_name.contains("SYNOPSIS"sv))
indentation = " "sv;
}
for (auto const& line : m_code.split('\n', SplitBehavior::KeepEmpty))
lines.append(ByteString::formatted("{}{}", indentation, line));
return lines;
}
RecursionDecision CodeBlock::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
rd = visitor.visit(m_code);
if (rd != RecursionDecision::Recurse)
return rd;
// Don't recurse on m_language and m_style.
// Normalize return value.
return RecursionDecision::Continue;
}
static Regex<ECMA262> open_fence_re("^ {0,3}(([\\`\\~])\\2{2,})\\s*([\\*_]*)\\s*([^\\*_\\s]*).*$");
static Regex<ECMA262> close_fence_re("^ {0,3}(([\\`\\~])\\2{2,})\\s*$");
static Optional<int> line_block_prefix(StringView const& line)
{
int characters = 0;
int indents = 0;
for (char ch : line) {
if (indents == 4)
break;
if (ch == ' ') {
++characters;
++indents;
} else if (ch == '\t') {
++characters;
indents = 4;
} else {
break;
}
}
if (indents == 4)
return characters;
return {};
}
OwnPtr<CodeBlock> CodeBlock::parse(LineIterator& lines, Heading* current_section)
{
if (lines.is_end())
return {};
StringView line = *lines;
if (open_fence_re.match(line).success)
return parse_backticks(lines, current_section);
if (line_block_prefix(line).has_value())
return parse_indent(lines);
return {};
}
OwnPtr<CodeBlock> CodeBlock::parse_backticks(LineIterator& lines, Heading* current_section)
{
StringView line = *lines;
// Our Markdown extension: we allow
// specifying a style and a language
// for a code block, like so:
//
// ```**sh**
// $ echo hello friends!
// ````
//
// The code block will be made bold,
// and if possible syntax-highlighted
// as appropriate for a shell script.
auto matches = open_fence_re.match(line).capture_group_matches[0];
auto fence = matches[0].view.string_view();
auto style = matches[2].view.string_view();
auto language = matches[3].view.string_view();
++lines;
StringBuilder builder;
while (true) {
if (lines.is_end())
break;
line = *lines;
++lines;
auto close_match = close_fence_re.match(line);
if (close_match.success) {
auto close_fence = close_match.capture_group_matches[0][0].view.string_view();
if (close_fence[0] == fence[0] && close_fence.length() >= fence.length())
break;
}
builder.append(line);
builder.append('\n');
}
return make<CodeBlock>(language, style, builder.to_byte_string(), current_section);
}
OwnPtr<CodeBlock> CodeBlock::parse_indent(LineIterator& lines)
{
StringBuilder builder;
while (true) {
if (lines.is_end())
break;
StringView line = *lines;
auto prefix_length = line_block_prefix(line);
if (!prefix_length.has_value())
break;
line = line.substring_view(prefix_length.value());
++lines;
builder.append(line);
builder.append('\n');
}
return make<CodeBlock>("", "", builder.to_byte_string(), nullptr);
}
}

View File

@ -1,44 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/OwnPtr.h>
#include <LibMarkdown/Block.h>
#include <LibMarkdown/Heading.h>
#include <LibMarkdown/LineIterator.h>
#include <LibMarkdown/Text.h>
namespace Markdown {
class CodeBlock final : public Block {
public:
CodeBlock(ByteString const& language, ByteString const& style, ByteString const& code, Heading* current_section)
: m_code(move(code))
, m_language(language)
, m_style(style)
, m_current_section(current_section)
{
}
virtual ~CodeBlock() override = default;
virtual ByteString render_to_html(bool tight = false) const override;
virtual Vector<ByteString> render_lines_for_terminal(size_t view_width = 0) const override;
virtual RecursionDecision walk(Visitor&) const override;
static OwnPtr<CodeBlock> parse(LineIterator& lines, Heading* current_section);
private:
ByteString m_code;
ByteString m_language;
ByteString m_style;
Heading* m_current_section;
static OwnPtr<CodeBlock> parse_backticks(LineIterator& lines, Heading* current_section);
static OwnPtr<CodeBlock> parse_indent(LineIterator& lines);
};
}

View File

@ -1,75 +0,0 @@
/*
* Copyright (c) 2021, Ben Wiederhake <BenWiederhake.GitHub@gmx.de>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Forward.h>
#include <AK/StringBuilder.h>
#include <LibMarkdown/CommentBlock.h>
#include <LibMarkdown/Visitor.h>
namespace Markdown {
ByteString CommentBlock::render_to_html(bool) const
{
StringBuilder builder;
builder.append("<!--"sv);
builder.append(escape_html_entities(m_comment));
// TODO: This is probably incorrect, because we technically need to escape "--" in some form. However, Browser does not care about this.
builder.append("-->\n"sv);
return builder.to_byte_string();
}
Vector<ByteString> CommentBlock::render_lines_for_terminal(size_t) const
{
return Vector<ByteString> {};
}
RecursionDecision CommentBlock::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
// Normalize return value.
return RecursionDecision::Continue;
}
OwnPtr<CommentBlock> CommentBlock::parse(LineIterator& lines)
{
if (lines.is_end())
return {};
constexpr auto comment_start = "<!--"sv;
constexpr auto comment_end = "-->"sv;
StringView line = *lines;
if (!line.starts_with(comment_start))
return {};
line = line.substring_view(comment_start.length());
StringBuilder builder;
while (true) {
// Invariant: At the beginning of the loop, `line` is valid and should be added to the builder.
bool ends_here = line.ends_with(comment_end);
if (ends_here)
line = line.substring_view(0, line.length() - comment_end.length());
builder.append(line);
if (!ends_here)
builder.append('\n');
++lines;
if (lines.is_end() || ends_here) {
break;
}
line = *lines;
}
return make<CommentBlock>(builder.to_byte_string());
}
}

View File

@ -1,34 +0,0 @@
/*
* Copyright (c) 2021, Ben Wiederhake <BenWiederhake.GitHub@gmx.de>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteString.h>
#include <AK/OwnPtr.h>
#include <LibMarkdown/Block.h>
#include <LibMarkdown/LineIterator.h>
namespace Markdown {
class CommentBlock final : public Block {
public:
CommentBlock(ByteString const& comment)
: m_comment(comment)
{
}
virtual ~CommentBlock() override = default;
virtual ByteString render_to_html(bool tight = false) const override;
virtual Vector<ByteString> render_lines_for_terminal(size_t view_width = 0) const override;
virtual RecursionDecision walk(Visitor&) const override;
static OwnPtr<CommentBlock> parse(LineIterator& lines);
private:
ByteString m_comment;
};
}

View File

@ -1,154 +0,0 @@
/*
* Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Forward.h>
#include <LibMarkdown/BlockQuote.h>
#include <LibMarkdown/CodeBlock.h>
#include <LibMarkdown/ContainerBlock.h>
#include <LibMarkdown/Heading.h>
#include <LibMarkdown/HorizontalRule.h>
#include <LibMarkdown/List.h>
#include <LibMarkdown/Paragraph.h>
#include <LibMarkdown/Table.h>
#include <LibMarkdown/Visitor.h>
namespace Markdown {
ByteString ContainerBlock::render_to_html(bool tight) const
{
StringBuilder builder;
for (size_t i = 0; i + 1 < m_blocks.size(); ++i) {
auto s = m_blocks[i]->render_to_html(tight);
builder.append(s);
}
// I don't like this edge case.
if (m_blocks.size() != 0) {
auto& block = m_blocks[m_blocks.size() - 1];
auto s = block->render_to_html(tight);
if (tight && dynamic_cast<Paragraph const*>(block.ptr())) {
builder.append(s.substring_view(0, s.length() - 1));
} else {
builder.append(s);
}
}
return builder.to_byte_string();
}
Vector<ByteString> ContainerBlock::render_lines_for_terminal(size_t view_width) const
{
Vector<ByteString> lines;
for (auto& block : m_blocks) {
for (auto& line : block->render_lines_for_terminal(view_width))
lines.append(move(line));
}
return lines;
}
RecursionDecision ContainerBlock::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
for (auto const& block : m_blocks) {
rd = block->walk(visitor);
if (rd == RecursionDecision::Break)
return rd;
}
return RecursionDecision::Continue;
}
template<class CodeBlock>
static bool try_parse_block(LineIterator& lines, Vector<NonnullOwnPtr<Block>>& blocks, Heading* current_section)
{
OwnPtr<CodeBlock> block = CodeBlock::parse(lines, current_section);
if (!block)
return false;
blocks.append(block.release_nonnull());
return true;
}
template<typename BlockType>
static bool try_parse_block(LineIterator& lines, Vector<NonnullOwnPtr<Block>>& blocks)
{
OwnPtr<BlockType> block = BlockType::parse(lines);
if (!block)
return false;
blocks.append(block.release_nonnull());
return true;
}
OwnPtr<ContainerBlock> ContainerBlock::parse(LineIterator& lines)
{
Vector<NonnullOwnPtr<Block>> blocks;
StringBuilder paragraph_text;
Heading* current_section = nullptr;
auto flush_paragraph = [&] {
if (paragraph_text.is_empty())
return;
auto paragraph = make<Paragraph>(Text::parse(paragraph_text.to_byte_string()));
blocks.append(move(paragraph));
paragraph_text.clear();
};
bool has_blank_lines = false;
bool has_trailing_blank_lines = false;
while (true) {
if (lines.is_end())
break;
if ((*lines).is_whitespace()) {
has_trailing_blank_lines = true;
++lines;
flush_paragraph();
continue;
} else {
has_blank_lines = has_blank_lines || has_trailing_blank_lines;
}
bool heading = false;
if ((heading = try_parse_block<Heading>(lines, blocks)))
current_section = dynamic_cast<Heading*>(blocks.last().ptr());
bool any = heading
|| try_parse_block<Table>(lines, blocks)
|| try_parse_block<HorizontalRule>(lines, blocks)
|| try_parse_block<List>(lines, blocks)
// CodeBlock needs to know the current section's name for proper indentation
|| try_parse_block<CodeBlock>(lines, blocks, current_section)
|| try_parse_block<CommentBlock>(lines, blocks)
|| try_parse_block<BlockQuote>(lines, blocks);
if (any) {
if (!paragraph_text.is_empty()) {
auto last_block = blocks.take_last();
flush_paragraph();
blocks.append(move(last_block));
}
continue;
}
if (!paragraph_text.is_empty())
paragraph_text.append('\n');
paragraph_text.append(*lines++);
}
flush_paragraph();
return make<ContainerBlock>(move(blocks), has_blank_lines, has_trailing_blank_lines);
}
}

View File

@ -1,45 +0,0 @@
/*
* Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteString.h>
#include <AK/OwnPtr.h>
#include <LibMarkdown/Block.h>
#include <LibMarkdown/LineIterator.h>
namespace Markdown {
class ContainerBlock final : public Block {
public:
ContainerBlock(Vector<NonnullOwnPtr<Block>> blocks, bool has_blank_lines, bool has_trailing_blank_lines)
: m_blocks(move(blocks))
, m_has_blank_lines(has_blank_lines)
, m_has_trailing_blank_lines(has_trailing_blank_lines)
{
}
virtual ~ContainerBlock() override = default;
virtual ByteString render_to_html(bool tight = false) const override;
virtual Vector<ByteString> render_lines_for_terminal(size_t view_width = 0) const override;
virtual RecursionDecision walk(Visitor&) const override;
static OwnPtr<ContainerBlock> parse(LineIterator& lines);
bool has_blank_lines() const { return m_has_blank_lines; }
bool has_trailing_blank_lines() const { return m_has_trailing_blank_lines; }
Vector<NonnullOwnPtr<Block>> const& blocks() const { return m_blocks; }
private:
Vector<NonnullOwnPtr<Block>> m_blocks;
bool m_has_blank_lines;
bool m_has_trailing_blank_lines;
};
}

View File

@ -1,73 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringBuilder.h>
#include <LibMarkdown/Document.h>
#include <LibMarkdown/LineIterator.h>
#include <LibMarkdown/Visitor.h>
namespace Markdown {
ByteString Document::render_to_html(StringView extra_head_contents) const
{
StringBuilder builder;
builder.append(R"~~~(<!DOCTYPE html>
<html>
<head>
<style>
code { white-space: pre; }
</style>
)~~~"sv);
if (!extra_head_contents.is_empty())
builder.append(extra_head_contents);
builder.append(R"~~~(
</head>
<body>
)~~~"sv);
builder.append(render_to_inline_html());
builder.append(R"~~~(
</body>
</html>)~~~"sv);
return builder.to_byte_string();
}
ByteString Document::render_to_inline_html() const
{
return m_container->render_to_html();
}
ErrorOr<String> Document::render_for_terminal(size_t view_width) const
{
StringBuilder builder;
for (auto& line : m_container->render_lines_for_terminal(view_width)) {
TRY(builder.try_append(line));
TRY(builder.try_append("\n"sv));
}
return builder.to_string();
}
RecursionDecision Document::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
return m_container->walk(visitor);
}
OwnPtr<Document> Document::parse(StringView str)
{
Vector<StringView> const lines_vec = str.lines();
LineIterator lines(lines_vec.begin());
return make<Document>(ContainerBlock::parse(lines));
}
}

View File

@ -1,44 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteString.h>
#include <AK/OwnPtr.h>
#include <AK/String.h>
#include <LibMarkdown/Block.h>
#include <LibMarkdown/ContainerBlock.h>
namespace Markdown {
class Document final {
public:
Document(OwnPtr<ContainerBlock> container)
: m_container(move(container))
{
}
ByteString render_to_html(StringView extra_head_contents = ""sv) const;
ByteString render_to_inline_html() const;
ErrorOr<String> render_for_terminal(size_t view_width = 0) const;
/*
* Walk recursively through the document tree. Returning `RecursionDecision::Recurse` from
* `Visitor::visit` proceeds with the next element of the pre-order walk, usually a child element.
* Returning `RecursionDecision::Continue` skips the subtree, and usually proceeds with the next
* sibling. Returning `RecursionDecision::Break` breaks the recursion, with no further calls to
* any of the `Visitor::visit` methods.
*
* Note that `walk()` will only return `RecursionDecision::Continue` or `RecursionDecision::Break`.
*/
RecursionDecision walk(Visitor&) const;
static OwnPtr<Document> parse(StringView);
private:
OwnPtr<ContainerBlock> m_container;
};
}

View File

@ -1,26 +0,0 @@
/*
* Copyright (c) 2021, Ben Wiederhake <BenWiederhake.GitHub@gmx.de>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
namespace Markdown {
class Block;
class Document;
class Text;
class BlockQuote;
class CodeBlock;
class ContainerBlock;
class Heading;
class HorizontalRule;
class List;
class Paragraph;
class Table;
class Visitor;
}

View File

@ -1,86 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Slugify.h>
#include <AK/StringBuilder.h>
#include <LibMarkdown/Heading.h>
#include <LibMarkdown/Visitor.h>
#include <LibUnicode/Normalize.h>
namespace Markdown {
ByteString Heading::render_to_html(bool) const
{
auto input = Unicode::normalize(m_text.render_for_raw_print(), Unicode::NormalizationForm::NFD);
auto slugified = MUST(AK::slugify(input));
return ByteString::formatted("<h{} id='{}'><a href='#{}'>#</a> {}</h{}>\n", m_level, slugified, slugified, m_text.render_to_html(), m_level);
}
Vector<ByteString> Heading::render_lines_for_terminal(size_t) const
{
StringBuilder builder;
builder.append("\n\033[0;31;1m"sv);
switch (m_level) {
case 1:
case 2:
builder.append(m_text.render_for_terminal().to_uppercase());
builder.append("\033[0m"sv);
break;
default:
builder.append(m_text.render_for_terminal());
builder.append("\033[0m"sv);
break;
}
return Vector<ByteString> { builder.to_byte_string() };
}
RecursionDecision Heading::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
return m_text.walk(visitor);
}
OwnPtr<Heading> Heading::parse(LineIterator& lines)
{
if (lines.is_end())
return {};
StringView line = *lines;
size_t indent = 0;
// Allow for up to 3 spaces of indentation.
// https://spec.commonmark.org/0.30/#example-68
for (size_t i = 0; i < 3; ++i) {
if (line[i] != ' ')
break;
++indent;
}
size_t level;
for (level = 0; indent + level < line.length(); level++) {
if (line[indent + level] != '#')
break;
}
if (!level || indent + level >= line.length() || line[indent + level] != ' ' || level > 6)
return {};
StringView title_view = line.substring_view(indent + level + 1);
auto text = Text::parse(title_view);
auto heading = make<Heading>(move(text), level);
++lines;
return heading;
}
}

View File

@ -1,39 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/OwnPtr.h>
#include <AK/StringView.h>
#include <AK/Vector.h>
#include <LibMarkdown/Block.h>
#include <LibMarkdown/LineIterator.h>
#include <LibMarkdown/Text.h>
namespace Markdown {
class Heading final : public Block {
public:
Heading(Text&& text, size_t level)
: m_text(move(text))
, m_level(level)
{
VERIFY(m_level > 0);
}
virtual ~Heading() override = default;
virtual ByteString render_to_html(bool tight = false) const override;
virtual Vector<ByteString> render_lines_for_terminal(size_t view_width = 0) const override;
virtual RecursionDecision walk(Visitor&) const override;
static OwnPtr<Heading> parse(LineIterator& lines);
private:
Text m_text;
size_t m_level { 0 };
};
}

View File

@ -1,55 +0,0 @@
/*
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteString.h>
#include <AK/StringBuilder.h>
#include <LibMarkdown/HorizontalRule.h>
#include <LibMarkdown/Visitor.h>
#include <LibRegex/Regex.h>
namespace Markdown {
ByteString HorizontalRule::render_to_html(bool) const
{
return "<hr />\n";
}
Vector<ByteString> HorizontalRule::render_lines_for_terminal(size_t view_width) const
{
StringBuilder builder(view_width + 1);
for (size_t i = 0; i < view_width; ++i)
builder.append('-');
builder.append("\n\n"sv);
return Vector<ByteString> { builder.to_byte_string() };
}
RecursionDecision HorizontalRule::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
// Normalize return value.
return RecursionDecision::Continue;
}
static Regex<ECMA262> thematic_break_re("^ {0,3}([\\*\\-_])\\s*(\\1\\s*){2,}$");
OwnPtr<HorizontalRule> HorizontalRule::parse(LineIterator& lines)
{
if (lines.is_end())
return {};
StringView line = *lines;
auto match = thematic_break_re.match(line);
if (!match.success)
return {};
++lines;
return make<HorizontalRule>();
}
}

View File

@ -1,29 +0,0 @@
/*
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/OwnPtr.h>
#include <AK/StringView.h>
#include <AK/Vector.h>
#include <LibMarkdown/Block.h>
#include <LibMarkdown/LineIterator.h>
namespace Markdown {
class HorizontalRule final : public Block {
public:
HorizontalRule() = default;
virtual ~HorizontalRule() override = default;
virtual ByteString render_to_html(bool tight = false) const override;
virtual Vector<ByteString> render_lines_for_terminal(size_t view_width = 0) const override;
virtual RecursionDecision walk(Visitor&) const override;
static OwnPtr<HorizontalRule> parse(LineIterator& lines);
};
}

View File

@ -1,64 +0,0 @@
/*
* Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibMarkdown/LineIterator.h>
namespace Markdown {
void LineIterator::reset_ignore_prefix()
{
for (auto& context : m_context_stack) {
context.ignore_prefix = false;
}
}
Optional<StringView> LineIterator::match_context(StringView line) const
{
bool is_ws = line.is_whitespace();
size_t offset = 0;
for (auto& context : m_context_stack) {
switch (context.type) {
case Context::Type::ListItem:
if (is_ws)
break;
if (offset + context.indent > line.length())
return {};
if (!context.ignore_prefix && !line.substring_view(offset, context.indent).is_whitespace())
return {};
offset += context.indent;
break;
case Context::Type::BlockQuote:
for (; offset < line.length() && line[offset] == ' '; ++offset) { }
if (offset >= line.length() || line[offset] != '>') {
return {};
}
++offset;
break;
}
if (offset > line.length())
return {};
}
return line.substring_view(offset);
}
bool LineIterator::is_end() const
{
return m_iterator.is_end() || !match_context(*m_iterator).has_value();
}
StringView LineIterator::operator*() const
{
auto line = match_context(*m_iterator);
VERIFY(line.has_value());
return line.value();
}
}

View File

@ -1,100 +0,0 @@
/*
* Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Iterator.h>
#include <AK/StringView.h>
#include <AK/Vector.h>
namespace Markdown {
template<typename T>
class FakePtr {
public:
FakePtr(T item)
: m_item(move(item))
{
}
T const* operator->() const { return &m_item; }
T* operator->() { return &m_item; }
private:
T m_item;
};
class LineIterator {
public:
struct Context {
enum class Type {
ListItem,
BlockQuote,
};
Type type;
size_t indent;
bool ignore_prefix;
static Context list_item(size_t indent) { return { Type::ListItem, indent, true }; }
static Context block_quote() { return { Type::BlockQuote, 0, false }; }
};
LineIterator(Vector<StringView>::ConstIterator const& lines)
: m_iterator(lines)
{
}
bool is_end() const;
StringView operator*() const;
LineIterator operator++()
{
reset_ignore_prefix();
++m_iterator;
return *this;
}
LineIterator operator++(int)
{
LineIterator tmp = *this;
reset_ignore_prefix();
++m_iterator;
return tmp;
}
LineIterator operator+(ptrdiff_t delta) const
{
LineIterator copy = *this;
copy.reset_ignore_prefix();
copy.m_iterator = copy.m_iterator + delta;
return copy;
}
LineIterator operator-(ptrdiff_t delta) const
{
LineIterator copy = *this;
copy.reset_ignore_prefix();
copy.m_iterator = copy.m_iterator - delta;
return copy;
}
ptrdiff_t operator-(LineIterator const& other) const { return m_iterator - other.m_iterator; }
FakePtr<StringView> operator->() const { return FakePtr<StringView>(operator*()); }
void push_context(Context context) { m_context_stack.append(move(context)); }
void pop_context() { m_context_stack.take_last(); }
private:
void reset_ignore_prefix();
Optional<StringView> match_context(StringView line) const;
Vector<StringView>::ConstIterator m_iterator;
Vector<Context> m_context_stack;
};
}

View File

@ -1,169 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Forward.h>
#include <AK/StringBuilder.h>
#include <LibMarkdown/List.h>
#include <LibMarkdown/Paragraph.h>
#include <LibMarkdown/Visitor.h>
namespace Markdown {
ByteString List::render_to_html(bool) const
{
StringBuilder builder;
char const* tag = m_is_ordered ? "ol" : "ul";
builder.appendff("<{}", tag);
if (m_start_number != 1)
builder.appendff(" start=\"{}\"", m_start_number);
builder.append(">\n"sv);
for (auto& item : m_items) {
builder.append("<li>"sv);
if (!m_is_tight || (item->blocks().size() != 0 && !dynamic_cast<Paragraph const*>(item->blocks()[0].ptr())))
builder.append('\n');
builder.append(item->render_to_html(m_is_tight));
builder.append("</li>\n"sv);
}
builder.appendff("</{}>\n", tag);
return builder.to_byte_string();
}
Vector<ByteString> List::render_lines_for_terminal(size_t view_width) const
{
Vector<ByteString> lines;
int i = 0;
for (auto& item : m_items) {
auto item_lines = item->render_lines_for_terminal(view_width);
auto first_line = item_lines.take_first();
StringBuilder builder;
builder.append(" "sv);
if (m_is_ordered)
builder.appendff("{}.", ++i);
else
builder.append('*');
auto item_indentation = builder.length();
builder.append(first_line);
lines.append(builder.to_byte_string());
for (auto& line : item_lines) {
builder.clear();
builder.append(ByteString::repeated(' ', item_indentation));
builder.append(line);
lines.append(builder.to_byte_string());
}
}
return lines;
}
RecursionDecision List::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
for (auto const& block : m_items) {
rd = block->walk(visitor);
if (rd == RecursionDecision::Break)
return rd;
}
return RecursionDecision::Continue;
}
OwnPtr<List> List::parse(LineIterator& lines)
{
Vector<OwnPtr<ContainerBlock>> items;
bool first = true;
bool is_ordered = false;
bool is_tight = true;
bool has_trailing_blank_lines = false;
size_t start_number = 1;
while (!lines.is_end()) {
size_t offset = 0;
StringView line = *lines;
bool appears_unordered = false;
while (offset < line.length() && line[offset] == ' ')
++offset;
if (offset + 2 <= line.length()) {
if (line[offset + 1] == ' ' && (line[offset] == '*' || line[offset] == '-' || line[offset] == '+')) {
appears_unordered = true;
offset++;
}
}
bool appears_ordered = false;
for (size_t i = offset; i < 10 && i < line.length(); i++) {
char ch = line[i];
if ('0' <= ch && ch <= '9')
continue;
if (ch == '.' || ch == ')')
if (i + 1 < line.length() && line[i + 1] == ' ') {
auto maybe_start_number = line.substring_view(offset, i - offset).to_number<size_t>();
if (!maybe_start_number.has_value())
break;
if (first)
start_number = maybe_start_number.value();
appears_ordered = true;
offset = i + 1;
}
break;
}
VERIFY(!(appears_unordered && appears_ordered));
if (!appears_unordered && !appears_ordered) {
if (first)
return {};
break;
}
while (offset < line.length() && line[offset] == ' ')
offset++;
if (first) {
is_ordered = appears_ordered;
} else if (appears_ordered != is_ordered) {
break;
}
is_tight = is_tight && !has_trailing_blank_lines;
lines.push_context(LineIterator::Context::list_item(offset));
auto list_item = ContainerBlock::parse(lines);
is_tight = is_tight && !list_item->has_blank_lines();
has_trailing_blank_lines = has_trailing_blank_lines || list_item->has_trailing_blank_lines();
items.append(move(list_item));
lines.pop_context();
first = false;
}
return make<List>(move(items), is_ordered, is_tight, start_number);
}
}

View File

@ -1,41 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/OwnPtr.h>
#include <LibMarkdown/Block.h>
#include <LibMarkdown/ContainerBlock.h>
#include <LibMarkdown/LineIterator.h>
namespace Markdown {
class List final : public Block {
public:
List(Vector<OwnPtr<ContainerBlock>> items, bool is_ordered, bool is_tight, size_t start_number)
: m_items(move(items))
, m_is_ordered(is_ordered)
, m_is_tight(is_tight)
, m_start_number(start_number)
{
}
virtual ~List() override = default;
virtual ByteString render_to_html(bool tight = false) const override;
virtual Vector<ByteString> render_lines_for_terminal(size_t view_width = 0) const override;
virtual RecursionDecision walk(Visitor&) const override;
static OwnPtr<List> parse(LineIterator& lines);
private:
Vector<OwnPtr<ContainerBlock>> m_items;
bool m_is_ordered { false };
bool m_is_tight { false };
size_t m_start_number { 1 };
};
}

View File

@ -1,45 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Forward.h>
#include <AK/StringBuilder.h>
#include <LibMarkdown/Paragraph.h>
#include <LibMarkdown/Visitor.h>
namespace Markdown {
ByteString Paragraph::render_to_html(bool tight) const
{
StringBuilder builder;
if (!tight)
builder.append("<p>"sv);
builder.append(m_text.render_to_html());
if (!tight)
builder.append("</p>"sv);
builder.append('\n');
return builder.to_byte_string();
}
Vector<ByteString> Paragraph::render_lines_for_terminal(size_t) const
{
return Vector<ByteString> { ByteString::formatted(" {}", m_text.render_for_terminal()), "" };
}
RecursionDecision Paragraph::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
return m_text.walk(visitor);
}
}

View File

@ -1,33 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/OwnPtr.h>
#include <LibMarkdown/Block.h>
#include <LibMarkdown/Text.h>
namespace Markdown {
class Paragraph final : public Block {
public:
Paragraph(Text text)
: m_text(move(text))
{
}
virtual ~Paragraph() override = default;
virtual ByteString render_to_html(bool tight = false) const override;
virtual Vector<ByteString> render_lines_for_terminal(size_t view_width = 0) const override;
virtual RecursionDecision walk(Visitor&) const override;
private:
Text m_text;
};
}

View File

@ -1,103 +0,0 @@
/*
* Copyright (c) 2023, Maciej <sppmacd@pm.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibMarkdown/SyntaxHighlighter.h>
namespace Markdown {
Syntax::Language SyntaxHighlighter::language() const
{
return Syntax::Language::Markdown;
}
Optional<StringView> SyntaxHighlighter::comment_prefix() const
{
return {};
}
Optional<StringView> SyntaxHighlighter::comment_suffix() const
{
return {};
}
enum class Token {
Default,
Header,
Code
};
void SyntaxHighlighter::rehighlight(Palette const& palette)
{
auto text = m_client->get_text();
Vector<Syntax::TextDocumentSpan> spans;
auto append_header = [&](Syntax::TextRange const& range) {
Gfx::TextAttributes attributes;
attributes.color = palette.base_text();
attributes.bold = true;
Syntax::TextDocumentSpan span {
.range = range,
.attributes = attributes,
.data = static_cast<u32>(Token::Header),
.is_skippable = false
};
spans.append(span);
};
auto append_code_block = [&](Syntax::TextRange const& range) {
Gfx::TextAttributes attributes;
attributes.color = palette.syntax_string();
Syntax::TextDocumentSpan span {
.range = range,
.attributes = attributes,
.data = static_cast<u32>(Token::Code),
.is_skippable = false
};
spans.append(span);
};
// Headers, code blocks
{
size_t line_index = 0;
Optional<size_t> code_block_start;
for (auto const& line : StringView(text).lines()) {
if (line.starts_with("```"sv)) {
if (code_block_start.has_value()) {
append_code_block({ { *code_block_start, 0 }, { line_index, line.length() } });
code_block_start = {};
} else {
code_block_start = line_index;
}
}
if (!code_block_start.has_value()) {
auto trimmed = line.trim_whitespace(TrimMode::Left);
size_t indent = line.length() - trimmed.length();
if (indent < 4 && trimmed.starts_with("#"sv)) {
append_header({ { line_index, 0 }, { line_index, line.length() } });
}
}
line_index++;
}
}
// TODO: Highlight text nodes (em, strong, link, image)
m_client->do_set_spans(spans);
}
Vector<SyntaxHighlighter::MatchingTokenPair> SyntaxHighlighter::matching_token_pairs_impl() const
{
return {};
}
bool SyntaxHighlighter::token_types_equal(u64 lhs, u64 rhs) const
{
return static_cast<Token>(lhs) == static_cast<Token>(rhs);
}
}

View File

@ -1,23 +0,0 @@
/*
* Copyright (c) 2023, Maciej <sppmacd@pm.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibSyntax/Highlighter.h>
namespace Markdown {
class SyntaxHighlighter : public Syntax::Highlighter {
virtual Syntax::Language language() const override;
virtual Optional<StringView> comment_prefix() const override;
virtual Optional<StringView> comment_suffix() const override;
virtual void rehighlight(Palette const&) override;
virtual Vector<MatchingTokenPair> matching_token_pairs_impl() const override;
virtual bool token_types_equal(u64, u64) const override;
};
}

View File

@ -1,265 +0,0 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <AK/StringBuilder.h>
#include <AK/Vector.h>
#include <LibMarkdown/Table.h>
#include <LibMarkdown/Visitor.h>
namespace Markdown {
Vector<ByteString> Table::render_lines_for_terminal(size_t view_width) const
{
auto unit_width_length = view_width == 0 ? 4 : ((float)(view_width - m_columns.size()) / (float)m_total_width);
StringBuilder builder;
Vector<ByteString> lines;
auto write_aligned = [&](auto const& text, auto width, auto alignment) {
size_t original_length = text.terminal_length();
auto string = text.render_for_terminal();
if (alignment == Alignment::Center) {
auto padding_length = (width - original_length) / 2;
// FIXME: We're using a StringView literal to bypass the compile-time AK::Format checking here, since it can't handle the "}}"
builder.appendff("{:{1}}"sv, "", (int)padding_length);
builder.append(string);
builder.appendff("{:{1}}"sv, "", (int)padding_length);
if ((width - original_length) % 2)
builder.append(' ');
} else {
// FIXME: We're using StringView literals to bypass the compile-time AK::Format checking here, since it can't handle the "}}"
builder.appendff(alignment == Alignment::Left ? "{:<{1}}"sv : "{:>{1}}"sv, string, (int)(width + (string.length() - original_length)));
}
};
bool first = true;
for (auto& col : m_columns) {
if (!first)
builder.append('|');
first = false;
size_t width = col.relative_width * unit_width_length;
write_aligned(col.header, width, col.alignment);
}
lines.append(builder.to_byte_string());
builder.clear();
for (size_t i = 0; i < view_width; ++i)
builder.append('-');
lines.append(builder.to_byte_string());
builder.clear();
for (size_t i = 0; i < m_row_count; ++i) {
bool first = true;
for (auto& col : m_columns) {
VERIFY(i < col.rows.size());
auto& cell = col.rows[i];
if (!first)
builder.append('|');
first = false;
size_t width = col.relative_width * unit_width_length;
write_aligned(cell, width, col.alignment);
}
lines.append(builder.to_byte_string());
builder.clear();
}
lines.append("");
return lines;
}
ByteString Table::render_to_html(bool) const
{
auto alignment_string = [](Alignment alignment) {
switch (alignment) {
case Alignment::Center:
return "center"sv;
case Alignment::Left:
return "left"sv;
case Alignment::Right:
return "right"sv;
}
VERIFY_NOT_REACHED();
};
StringBuilder builder;
builder.append("<table>"sv);
builder.append("<thead>"sv);
builder.append("<tr>"sv);
for (auto& column : m_columns) {
builder.appendff("<th style='text-align: {}'>", alignment_string(column.alignment));
builder.append(column.header.render_to_html());
builder.append("</th>"sv);
}
builder.append("</tr>"sv);
builder.append("</thead>"sv);
builder.append("<tbody>"sv);
for (size_t i = 0; i < m_row_count; ++i) {
builder.append("<tr>"sv);
for (auto& column : m_columns) {
VERIFY(i < column.rows.size());
builder.appendff("<td style='text-align: {}'>", alignment_string(column.alignment));
builder.append(column.rows[i].render_to_html());
builder.append("</td>"sv);
}
builder.append("</tr>"sv);
}
builder.append("</tbody>"sv);
builder.append("</table>"sv);
return builder.to_byte_string();
}
RecursionDecision Table::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
for (auto const& column : m_columns) {
rd = column.walk(visitor);
if (rd == RecursionDecision::Break)
return rd;
}
return RecursionDecision::Continue;
}
OwnPtr<Table> Table::parse(LineIterator& lines)
{
auto peek_it = lines;
auto first_line = *peek_it;
if (!first_line.starts_with('|'))
return {};
++peek_it;
if (peek_it.is_end())
return {};
auto header_segments = first_line.split_view('|', SplitBehavior::KeepEmpty);
auto header_delimiters = peek_it->split_view('|', SplitBehavior::KeepEmpty);
if (!header_segments.is_empty())
header_segments.take_first();
if (!header_segments.is_empty() && header_segments.last().is_empty())
header_segments.take_last();
if (!header_delimiters.is_empty())
header_delimiters.take_first();
if (!header_delimiters.is_empty() && header_delimiters.last().is_empty())
header_delimiters.take_last();
++peek_it;
if (header_delimiters.size() != header_segments.size())
return {};
if (header_delimiters.is_empty())
return {};
size_t total_width = 0;
auto table = make<Table>();
table->m_columns.resize(header_delimiters.size());
for (size_t i = 0; i < header_segments.size(); ++i) {
auto text = Text::parse(header_segments[i]);
auto& column = table->m_columns[i];
column.header = move(text);
auto delimiter = header_delimiters[i].trim_whitespace();
auto align_left = delimiter.starts_with(':');
auto align_right = delimiter != ":" && delimiter.ends_with(':');
if (align_left)
delimiter = delimiter.substring_view(1, delimiter.length() - 1);
if (align_right)
delimiter = delimiter.substring_view(0, delimiter.length() - 1);
if (align_left && align_right)
column.alignment = Alignment::Center;
else if (align_right)
column.alignment = Alignment::Right;
else
column.alignment = Alignment::Left;
size_t relative_width = delimiter.length();
for (auto ch : delimiter) {
if (ch != '-') {
dbgln_if(MARKDOWN_DEBUG, "Invalid character _{}_ in table heading delimiter (ignored)", ch);
--relative_width;
}
}
column.relative_width = relative_width;
total_width += relative_width;
}
table->m_total_width = total_width;
for (off_t i = 0; i < peek_it - lines; ++i)
++lines;
size_t row_count = 0;
++lines;
while (!lines.is_end()) {
auto line = *lines;
if (!line.starts_with('|'))
break;
++lines;
auto segments = line.split_view('|', SplitBehavior::KeepEmpty);
segments.take_first();
if (!segments.is_empty() && segments.last().is_empty())
segments.take_last();
++row_count;
for (size_t i = 0; i < header_segments.size(); ++i) {
if (i >= segments.size()) {
// Ran out of segments, but still have headers.
// Just make an empty cell.
table->m_columns[i].rows.append(Text::parse(""sv));
} else {
auto text = Text::parse(segments[i]);
table->m_columns[i].rows.append(move(text));
}
}
}
table->m_row_count = row_count;
return table;
}
RecursionDecision Table::Column::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
rd = header.walk(visitor);
if (rd != RecursionDecision::Recurse)
return rd;
for (auto const& row : rows) {
rd = row.walk(visitor);
if (rd == RecursionDecision::Break)
return rd;
}
return RecursionDecision::Continue;
}
}

View File

@ -1,49 +0,0 @@
/*
* Copyright (c) 2020-2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/OwnPtr.h>
#include <LibMarkdown/Block.h>
#include <LibMarkdown/LineIterator.h>
#include <LibMarkdown/Text.h>
namespace Markdown {
class Table final : public Block {
public:
enum class Alignment {
Center,
Left,
Right,
};
struct Column {
Text header;
Vector<Text> rows;
Alignment alignment { Alignment::Left };
size_t relative_width { 0 };
RecursionDecision walk(Visitor&) const;
};
Table() = default;
virtual ~Table() override = default;
virtual ByteString render_to_html(bool tight = false) const override;
virtual Vector<ByteString> render_lines_for_terminal(size_t view_width = 0) const override;
virtual RecursionDecision walk(Visitor&) const override;
static OwnPtr<Table> parse(LineIterator& lines);
Vector<Column> const& columns() const { return m_columns; }
private:
Vector<Column> m_columns;
size_t m_total_width { 1 };
size_t m_row_count { 0 };
};
}

View File

@ -1,724 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ScopeGuard.h>
#include <AK/StringBuilder.h>
#include <LibMarkdown/Text.h>
#include <LibMarkdown/Visitor.h>
#include <ctype.h>
#include <string.h>
namespace Markdown {
void Text::EmphasisNode::render_to_html(StringBuilder& builder) const
{
builder.append((strong) ? "<strong>"sv : "<em>"sv);
child->render_to_html(builder);
builder.append((strong) ? "</strong>"sv : "</em>"sv);
}
void Text::EmphasisNode::render_for_terminal(StringBuilder& builder) const
{
if (strong) {
builder.append("\e[1m"sv);
child->render_for_terminal(builder);
builder.append("\e[22m"sv);
} else {
builder.append("\e[3m"sv);
child->render_for_terminal(builder);
builder.append("\e[23m"sv);
}
}
void Text::EmphasisNode::render_for_raw_print(StringBuilder& builder) const
{
child->render_for_raw_print(builder);
}
size_t Text::EmphasisNode::terminal_length() const
{
return child->terminal_length();
}
RecursionDecision Text::EmphasisNode::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
return child->walk(visitor);
}
void Text::CodeNode::render_to_html(StringBuilder& builder) const
{
builder.append("<code>"sv);
code->render_to_html(builder);
builder.append("</code>"sv);
}
void Text::CodeNode::render_for_terminal(StringBuilder& builder) const
{
builder.append("\e[1m"sv);
code->render_for_terminal(builder);
builder.append("\e[22m"sv);
}
void Text::CodeNode::render_for_raw_print(StringBuilder& builder) const
{
code->render_for_raw_print(builder);
}
size_t Text::CodeNode::terminal_length() const
{
return code->terminal_length();
}
RecursionDecision Text::CodeNode::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
return code->walk(visitor);
}
void Text::BreakNode::render_to_html(StringBuilder& builder) const
{
builder.append("<br />"sv);
}
void Text::BreakNode::render_for_terminal(StringBuilder&) const
{
}
void Text::BreakNode::render_for_raw_print(StringBuilder&) const
{
}
size_t Text::BreakNode::terminal_length() const
{
return 0;
}
RecursionDecision Text::BreakNode::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
// Normalize return value
return RecursionDecision::Continue;
}
void Text::TextNode::render_to_html(StringBuilder& builder) const
{
builder.append(escape_html_entities(text));
}
void Text::TextNode::render_for_raw_print(StringBuilder& builder) const
{
builder.append(text);
}
void Text::TextNode::render_for_terminal(StringBuilder& builder) const
{
if (collapsible && (text == "\n" || text.is_whitespace())) {
builder.append(' ');
} else {
builder.append(text);
}
}
size_t Text::TextNode::terminal_length() const
{
if (collapsible && text.is_whitespace()) {
return 1;
}
return text.length();
}
RecursionDecision Text::TextNode::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
rd = visitor.visit(text);
if (rd != RecursionDecision::Recurse)
return rd;
// Normalize return value
return RecursionDecision::Continue;
}
void Text::LinkNode::render_to_html(StringBuilder& builder) const
{
if (is_image) {
builder.append("<img src=\""sv);
builder.append(escape_html_entities(href));
if (has_image_dimensions()) {
builder.append("\" style=\""sv);
if (image_width.has_value())
builder.appendff("width: {}px;", *image_width);
if (image_height.has_value())
builder.appendff("height: {}px;", *image_height);
}
builder.append("\" alt=\""sv);
text->render_to_html(builder);
builder.append("\" >"sv);
} else {
builder.append("<a href=\""sv);
builder.append(escape_html_entities(href));
builder.append("\">"sv);
text->render_to_html(builder);
builder.append("</a>"sv);
}
}
void Text::LinkNode::render_for_raw_print(StringBuilder& builder) const
{
text->render_for_raw_print(builder);
}
void Text::LinkNode::render_for_terminal(StringBuilder& builder) const
{
bool is_linked = href.contains("://"sv);
if (is_linked) {
builder.append("\033[0;34m\e]8;;"sv);
builder.append(href);
builder.append("\e\\"sv);
}
text->render_for_terminal(builder);
if (is_linked) {
builder.appendff(" <{}>", href);
builder.append("\033]8;;\033\\\033[0m"sv);
}
}
size_t Text::LinkNode::terminal_length() const
{
return text->terminal_length();
}
RecursionDecision Text::LinkNode::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
// Don't recurse on href.
return text->walk(visitor);
}
void Text::MultiNode::render_to_html(StringBuilder& builder) const
{
for (auto& child : children) {
child->render_to_html(builder);
}
}
void Text::MultiNode::render_for_raw_print(StringBuilder& builder) const
{
for (auto& child : children) {
child->render_for_raw_print(builder);
}
}
void Text::MultiNode::render_for_terminal(StringBuilder& builder) const
{
for (auto& child : children) {
child->render_for_terminal(builder);
}
}
size_t Text::MultiNode::terminal_length() const
{
size_t length = 0;
for (auto& child : children) {
length += child->terminal_length();
}
return length;
}
RecursionDecision Text::MultiNode::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
for (auto const& child : children) {
rd = child->walk(visitor);
if (rd == RecursionDecision::Break)
return rd;
}
return RecursionDecision::Continue;
}
void Text::StrikeThroughNode::render_to_html(StringBuilder& builder) const
{
builder.append("<del>"sv);
striked_text->render_to_html(builder);
builder.append("</del>"sv);
}
void Text::StrikeThroughNode::render_for_raw_print(StringBuilder& builder) const
{
striked_text->render_for_raw_print(builder);
}
void Text::StrikeThroughNode::render_for_terminal(StringBuilder& builder) const
{
builder.append("\e[9m"sv);
striked_text->render_for_terminal(builder);
builder.append("\e[29m"sv);
}
size_t Text::StrikeThroughNode::terminal_length() const
{
return striked_text->terminal_length();
}
RecursionDecision Text::StrikeThroughNode::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
return striked_text->walk(visitor);
}
size_t Text::terminal_length() const
{
return m_node->terminal_length();
}
ByteString Text::render_to_html() const
{
StringBuilder builder;
m_node->render_to_html(builder);
return builder.to_byte_string().trim(" \n\t"sv);
}
ByteString Text::render_for_raw_print() const
{
StringBuilder builder;
m_node->render_for_raw_print(builder);
return builder.to_byte_string().trim(" \n\t"sv);
}
ByteString Text::render_for_terminal() const
{
StringBuilder builder;
m_node->render_for_terminal(builder);
return builder.to_byte_string().trim(" \n\t"sv);
}
RecursionDecision Text::walk(Visitor& visitor) const
{
RecursionDecision rd = visitor.visit(*this);
if (rd != RecursionDecision::Recurse)
return rd;
return m_node->walk(visitor);
}
Text Text::parse(StringView str)
{
Text text;
auto const tokens = tokenize(str);
auto iterator = tokens.begin();
text.m_node = parse_sequence(iterator, false);
return text;
}
static bool flanking(StringView str, size_t start, size_t end, int dir)
{
ssize_t next = ((dir > 0) ? end : start) + dir;
if (next < 0 || next >= (ssize_t)str.length())
return false;
if (isspace(str[next]))
return false;
if (!ispunct(str[next]))
return true;
ssize_t prev = ((dir > 0) ? start : end) - dir;
if (prev < 0 || prev >= (ssize_t)str.length())
return true;
return isspace(str[prev]) || ispunct(str[prev]);
}
Vector<Text::Token> Text::tokenize(StringView str)
{
Vector<Token> tokens;
StringBuilder current_token;
auto flush_run = [&](bool left_flanking, bool right_flanking, bool punct_before, bool punct_after, bool is_run) {
if (current_token.is_empty())
return;
tokens.append({
current_token.to_byte_string(),
left_flanking,
right_flanking,
punct_before,
punct_after,
is_run,
});
current_token.clear();
};
auto flush_token = [&]() {
flush_run(false, false, false, false, false);
};
bool in_space = false;
for (size_t offset = 0; offset < str.length(); ++offset) {
auto has = [&](StringView seq) {
if (offset + seq.length() > str.length())
return false;
return str.substring_view(offset, seq.length()) == seq;
};
auto expect = [&](StringView seq) {
VERIFY(has(seq));
flush_token();
current_token.append(seq);
flush_token();
offset += seq.length() - 1;
};
char ch = str[offset];
if (ch != ' ' && in_space) {
flush_token();
in_space = false;
}
if (ch == '\\' && offset + 1 < str.length() && ispunct(str[offset + 1])) {
current_token.append(str[offset + 1]);
++offset;
} else if (ch == '*' || ch == '_' || ch == '`' || ch == '~') {
flush_token();
char delim = ch;
size_t run_offset;
for (run_offset = offset; run_offset < str.length() && str[run_offset] == delim; ++run_offset) {
current_token.append(str[run_offset]);
}
flush_run(flanking(str, offset, run_offset - 1, +1),
flanking(str, offset, run_offset - 1, -1),
offset > 0 && ispunct(str[offset - 1]),
run_offset < str.length() && ispunct(str[run_offset]),
true);
offset = run_offset - 1;
} else if (ch == ' ') {
if (!in_space) {
flush_token();
in_space = true;
}
current_token.append(ch);
} else if (has("\n"sv)) {
expect("\n"sv);
} else if (has("["sv)) {
expect("["sv);
} else if (has("!["sv)) {
expect("!["sv);
} else if (has("]("sv)) {
expect("]("sv);
} else if (has(")"sv)) {
expect(")"sv);
} else {
current_token.append(ch);
}
}
flush_token();
return tokens;
}
NonnullOwnPtr<Text::MultiNode> Text::parse_sequence(Vector<Token>::ConstIterator& tokens, bool in_link)
{
auto node = make<MultiNode>();
for (; !tokens.is_end(); ++tokens) {
if (tokens->is_space()) {
node->children.append(parse_break(tokens));
} else if (*tokens == "\n"sv) {
node->children.append(parse_newline(tokens));
} else if (tokens->is_run) {
switch (tokens->run_char()) {
case '*':
case '_':
node->children.append(parse_emph(tokens, in_link));
break;
case '`':
node->children.append(parse_code(tokens));
break;
case '~':
node->children.append(parse_strike_through(tokens));
break;
}
} else if (*tokens == "["sv || *tokens == "!["sv) {
node->children.append(parse_link(tokens));
} else if (in_link && *tokens == "]("sv) {
return node;
} else {
node->children.append(make<TextNode>(tokens->data));
}
if (in_link && !tokens.is_end() && *tokens == "]("sv)
return node;
if (tokens.is_end())
break;
}
return node;
}
NonnullOwnPtr<Text::Node> Text::parse_break(Vector<Token>::ConstIterator& tokens)
{
auto next_tok = tokens + 1;
if (next_tok.is_end() || *next_tok != "\n"sv)
return make<TextNode>(tokens->data);
if (tokens->data.length() >= 2)
return make<BreakNode>();
return make<MultiNode>();
}
NonnullOwnPtr<Text::Node> Text::parse_newline(Vector<Token>::ConstIterator& tokens)
{
auto node = make<TextNode>(tokens->data);
auto next_tok = tokens + 1;
if (!next_tok.is_end() && next_tok->is_space())
// Skip whitespace after newline.
++tokens;
return node;
}
bool Text::can_open(Token const& opening)
{
return (opening.run_char() == '~' && opening.left_flanking) || (opening.run_char() == '*' && opening.left_flanking) || (opening.run_char() == '_' && opening.left_flanking && (!opening.right_flanking || opening.punct_before));
}
bool Text::can_close_for(Token const& opening, Text::Token const& closing)
{
if (opening.run_char() != closing.run_char())
return false;
if (opening.run_length() != closing.run_length())
return false;
return (opening.run_char() == '~' && closing.right_flanking) || (opening.run_char() == '*' && closing.right_flanking) || (opening.run_char() == '_' && closing.right_flanking && (!closing.left_flanking || closing.punct_after));
}
NonnullOwnPtr<Text::Node> Text::parse_emph(Vector<Token>::ConstIterator& tokens, bool in_link)
{
auto opening = *tokens;
// Check that the opening delimiter run is properly flanking.
if (!can_open(opening))
return make<TextNode>(opening.data);
auto child = make<MultiNode>();
for (++tokens; !tokens.is_end(); ++tokens) {
if (tokens->is_space()) {
child->children.append(parse_break(tokens));
} else if (*tokens == "\n"sv) {
child->children.append(parse_newline(tokens));
} else if (tokens->is_run) {
if (can_close_for(opening, *tokens)) {
return make<EmphasisNode>(opening.run_length() >= 2, move(child));
}
switch (tokens->run_char()) {
case '*':
case '_':
child->children.append(parse_emph(tokens, in_link));
break;
case '`':
child->children.append(parse_code(tokens));
break;
case '~':
child->children.append(parse_strike_through(tokens));
break;
}
} else if (*tokens == "["sv || *tokens == "!["sv) {
child->children.append(parse_link(tokens));
} else if (in_link && *tokens == "]("sv) {
child->children.prepend(make<TextNode>(opening.data));
return child;
} else {
child->children.append(make<TextNode>(tokens->data));
}
if (in_link && !tokens.is_end() && *tokens == "]("sv) {
child->children.prepend(make<TextNode>(opening.data));
return child;
}
if (tokens.is_end())
break;
}
child->children.prepend(make<TextNode>(opening.data));
return child;
}
NonnullOwnPtr<Text::Node> Text::parse_code(Vector<Token>::ConstIterator& tokens)
{
auto opening = *tokens;
auto is_closing = [&](Token const& token) {
return token.is_run && token.run_char() == '`' && token.run_length() == opening.run_length();
};
bool is_all_whitespace = true;
auto code = make<MultiNode>();
for (auto iterator = tokens + 1; !iterator.is_end(); ++iterator) {
if (is_closing(*iterator)) {
tokens = iterator;
// Strip first and last space, when appropriate.
if (!is_all_whitespace) {
auto& first = dynamic_cast<TextNode&>(*code->children.first());
auto& last = dynamic_cast<TextNode&>(*code->children.last());
if (first.text.starts_with(' ') && last.text.ends_with(' ')) {
first.text = first.text.substring(1);
last.text = last.text.substring(0, last.text.length() - 1);
}
}
return make<CodeNode>(move(code));
}
is_all_whitespace = is_all_whitespace && iterator->data.is_whitespace();
code->children.append(make<TextNode>((*iterator == "\n"sv) ? " " : iterator->data, false));
}
return make<TextNode>(opening.data);
}
NonnullOwnPtr<Text::Node> Text::parse_link(Vector<Token>::ConstIterator& tokens)
{
auto opening = *tokens++;
bool is_image = opening == "!["sv;
auto link_text = parse_sequence(tokens, true);
if (tokens.is_end() || *tokens != "]("sv) {
link_text->children.prepend(make<TextNode>(opening.data));
return link_text;
}
auto separator = *tokens;
VERIFY(separator == "]("sv);
Optional<int> image_width;
Optional<int> image_height;
auto parse_image_dimensions = [&](StringView dimensions) -> bool {
if (!dimensions.starts_with('='))
return false;
ArmedScopeGuard clear_image_dimensions = [&] {
image_width = {};
image_height = {};
};
auto dimension_seperator = dimensions.find('x', 1);
if (!dimension_seperator.has_value())
return false;
auto width_string = dimensions.substring_view(1, *dimension_seperator - 1);
if (!width_string.is_empty()) {
auto width = width_string.to_number<int>();
if (!width.has_value())
return false;
image_width = width;
}
auto height_start = *dimension_seperator + 1;
if (height_start < dimensions.length()) {
auto height_string = dimensions.substring_view(height_start);
auto height = height_string.to_number<int>();
if (!height.has_value())
return false;
image_height = height;
}
clear_image_dimensions.disarm();
return true;
};
StringBuilder address;
for (auto iterator = tokens + 1; !iterator.is_end(); ++iterator) {
// FIXME: What to do if there's multiple dimension tokens?
if (is_image && !address.is_empty() && parse_image_dimensions(iterator->data))
continue;
if (*iterator == ")"sv) {
tokens = iterator;
ByteString href = address.to_byte_string().trim_whitespace();
// Add file:// if the link is an absolute path otherwise it will be assumed relative.
if (AK::StringUtils::starts_with(href, "/"sv, CaseSensitivity::CaseSensitive))
href = ByteString::formatted("file://{}", href);
return make<LinkNode>(is_image, move(link_text), move(href), image_width, image_height);
}
address.append(iterator->data);
}
link_text->children.prepend(make<TextNode>(opening.data));
link_text->children.append(make<TextNode>(separator.data));
return link_text;
}
NonnullOwnPtr<Text::Node> Text::parse_strike_through(Vector<Token>::ConstIterator& tokens)
{
auto opening = *tokens;
auto is_closing = [&](Token const& token) {
return token.is_run && token.run_char() == '~' && token.run_length() == opening.run_length();
};
bool is_all_whitespace = true;
auto striked_text = make<MultiNode>();
for (auto iterator = tokens + 1; !iterator.is_end(); ++iterator) {
if (is_closing(*iterator)) {
tokens = iterator;
if (!is_all_whitespace) {
auto& first = dynamic_cast<TextNode&>(*striked_text->children.first());
auto& last = dynamic_cast<TextNode&>(*striked_text->children.last());
if (first.text.starts_with(' ') && last.text.ends_with(' ')) {
first.text = first.text.substring(1);
last.text = last.text.substring(0, last.text.length() - 1);
}
}
return make<StrikeThroughNode>(move(striked_text));
}
is_all_whitespace = is_all_whitespace && iterator->data.is_whitespace();
striked_text->children.append(make<TextNode>((*iterator == "\n"sv) ? " " : iterator->data, false));
}
return make<TextNode>(opening.data);
}
}

View File

@ -1,212 +0,0 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteString.h>
#include <AK/Noncopyable.h>
#include <AK/OwnPtr.h>
#include <AK/RecursionDecision.h>
#include <AK/Vector.h>
#include <LibMarkdown/Forward.h>
namespace Markdown {
class Text final {
public:
class Node {
public:
virtual void render_to_html(StringBuilder& builder) const = 0;
virtual void render_for_terminal(StringBuilder& builder) const = 0;
virtual void render_for_raw_print(StringBuilder& builder) const = 0;
virtual size_t terminal_length() const = 0;
virtual RecursionDecision walk(Visitor&) const = 0;
virtual ~Node() = default;
};
class EmphasisNode : public Node {
public:
bool strong;
NonnullOwnPtr<Node> child;
EmphasisNode(bool strong, NonnullOwnPtr<Node> child)
: strong(strong)
, child(move(child))
{
}
virtual void render_to_html(StringBuilder& builder) const override;
virtual void render_for_terminal(StringBuilder& builder) const override;
virtual void render_for_raw_print(StringBuilder& builder) const override;
virtual size_t terminal_length() const override;
virtual RecursionDecision walk(Visitor&) const override;
};
class CodeNode : public Node {
public:
NonnullOwnPtr<Node> code;
CodeNode(NonnullOwnPtr<Node> code)
: code(move(code))
{
}
virtual void render_to_html(StringBuilder& builder) const override;
virtual void render_for_terminal(StringBuilder& builder) const override;
virtual void render_for_raw_print(StringBuilder& builder) const override;
virtual size_t terminal_length() const override;
virtual RecursionDecision walk(Visitor&) const override;
};
class BreakNode : public Node {
public:
virtual void render_to_html(StringBuilder& builder) const override;
virtual void render_for_terminal(StringBuilder& builder) const override;
virtual void render_for_raw_print(StringBuilder& builder) const override;
virtual size_t terminal_length() const override;
virtual RecursionDecision walk(Visitor&) const override;
};
class TextNode : public Node {
public:
ByteString text;
bool collapsible;
TextNode(StringView text)
: text(text)
, collapsible(true)
{
}
TextNode(StringView text, bool collapsible)
: text(text)
, collapsible(collapsible)
{
}
virtual void render_to_html(StringBuilder& builder) const override;
virtual void render_for_terminal(StringBuilder& builder) const override;
virtual void render_for_raw_print(StringBuilder& builder) const override;
virtual size_t terminal_length() const override;
virtual RecursionDecision walk(Visitor&) const override;
};
class LinkNode : public Node {
public:
bool is_image;
NonnullOwnPtr<Node> text;
ByteString href;
Optional<int> image_width;
Optional<int> image_height;
LinkNode(bool is_image, NonnullOwnPtr<Node> text, ByteString href, Optional<int> image_width, Optional<int> image_height)
: is_image(is_image)
, text(move(text))
, href(move(href))
, image_width(image_width)
, image_height(image_height)
{
}
bool has_image_dimensions() const
{
return image_width.has_value() || image_height.has_value();
}
virtual void render_to_html(StringBuilder& builder) const override;
virtual void render_for_terminal(StringBuilder& builder) const override;
virtual void render_for_raw_print(StringBuilder& builder) const override;
virtual size_t terminal_length() const override;
virtual RecursionDecision walk(Visitor&) const override;
};
class MultiNode : public Node {
public:
Vector<NonnullOwnPtr<Node>> children;
virtual void render_to_html(StringBuilder& builder) const override;
virtual void render_for_terminal(StringBuilder& builder) const override;
virtual void render_for_raw_print(StringBuilder& builder) const override;
virtual size_t terminal_length() const override;
virtual RecursionDecision walk(Visitor&) const override;
};
class StrikeThroughNode : public Node {
public:
NonnullOwnPtr<Node> striked_text;
StrikeThroughNode(NonnullOwnPtr<Node> striked_text)
: striked_text(move(striked_text))
{
}
virtual void render_to_html(StringBuilder& builder) const override;
virtual void render_for_terminal(StringBuilder& builder) const override;
virtual void render_for_raw_print(StringBuilder& builder) const override;
virtual size_t terminal_length() const override;
virtual RecursionDecision walk(Visitor&) const override;
};
size_t terminal_length() const;
ByteString render_to_html() const;
ByteString render_for_terminal() const;
ByteString render_for_raw_print() const;
RecursionDecision walk(Visitor&) const;
static Text parse(StringView);
private:
struct Token {
ByteString data;
// Flanking basically means that a delimiter run has a non-whitespace,
// non-punctuation character on the corresponding side. For a more exact
// definition, see the CommonMark spec.
bool left_flanking;
bool right_flanking;
bool punct_before;
bool punct_after;
// is_run indicates that this token is a 'delimiter run'. A delimiter
// run occurs when several of the same syntactical character ('`', '_',
// or '*') occur in a row.
bool is_run;
char run_char() const
{
VERIFY(is_run);
return data[0];
}
char run_length() const
{
VERIFY(is_run);
return data.length();
}
bool is_space() const
{
return data[0] == ' ';
}
bool operator==(StringView str) const { return str == data; }
};
static Vector<Token> tokenize(StringView);
static bool can_open(Token const& opening);
static bool can_close_for(Token const& opening, Token const& closing);
static NonnullOwnPtr<MultiNode> parse_sequence(Vector<Token>::ConstIterator& tokens, bool in_link);
static NonnullOwnPtr<Node> parse_break(Vector<Token>::ConstIterator& tokens);
static NonnullOwnPtr<Node> parse_newline(Vector<Token>::ConstIterator& tokens);
static NonnullOwnPtr<Node> parse_emph(Vector<Token>::ConstIterator& tokens, bool in_link);
static NonnullOwnPtr<Node> parse_code(Vector<Token>::ConstIterator& tokens);
static NonnullOwnPtr<Node> parse_link(Vector<Token>::ConstIterator& tokens);
static NonnullOwnPtr<Node> parse_strike_through(Vector<Token>::ConstIterator& tokens);
OwnPtr<Node> m_node;
};
}

View File

@ -1,53 +0,0 @@
/*
* Copyright (c) 2021, Ben Wiederhake <BenWiederhake.GitHub@gmx.de>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/RecursionDecision.h>
#include <LibMarkdown/BlockQuote.h>
#include <LibMarkdown/CodeBlock.h>
#include <LibMarkdown/CommentBlock.h>
#include <LibMarkdown/Document.h>
#include <LibMarkdown/Heading.h>
#include <LibMarkdown/HorizontalRule.h>
#include <LibMarkdown/List.h>
#include <LibMarkdown/Paragraph.h>
#include <LibMarkdown/Table.h>
namespace Markdown {
class Visitor {
public:
Visitor() = default;
virtual ~Visitor() = default;
virtual RecursionDecision visit(Document const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(BlockQuote const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(CodeBlock const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(CommentBlock const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(ContainerBlock const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Heading const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(HorizontalRule const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(List const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Paragraph const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Table const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Table::Column const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Text const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Text::BreakNode const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Text::CodeNode const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Text::EmphasisNode const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Text::LinkNode const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Text::MultiNode const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Text::StrikeThroughNode const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(Text::TextNode const&) { return RecursionDecision::Recurse; }
virtual RecursionDecision visit(ByteString const&) { return RecursionDecision::Recurse; }
};
}

View File

@ -750,7 +750,7 @@ set(GENERATED_SOURCES
serenity_lib(LibWeb web)
target_link_libraries(LibWeb PRIVATE LibCore LibCrypto LibJS LibMarkdown LibHTTP LibGemini LibGfx LibIPC LibLocale LibRegex LibSyntax LibTextCodec LibUnicode LibAudio LibVideo LibWasm LibXML LibIDL LibURL LibTLS)
target_link_libraries(LibWeb PRIVATE LibCore LibCrypto LibJS LibHTTP LibGemini LibGfx LibIPC LibLocale LibRegex LibSyntax LibTextCodec LibUnicode LibAudio LibVideo LibWasm LibXML LibIDL LibURL LibTLS)
if (HAS_ACCELERATED_GRAPHICS)
target_link_libraries(LibWeb PRIVATE ${ACCEL_GFX_LIBS})

View File

@ -10,7 +10,6 @@
#include <AK/LexicalPath.h>
#include <LibGemini/Document.h>
#include <LibGfx/ImageFormats/ImageDecoder.h>
#include <LibMarkdown/Document.h>
#include <LibTextCodec/Decoder.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/DocumentLoading.h>
@ -37,74 +36,6 @@ static void convert_to_xml_error_document(DOM::Document& document, String error_
MUST(document.append_child(html_element));
}
static WebIDL::ExceptionOr<JS::NonnullGCPtr<DOM::Document>> load_markdown_document(HTML::NavigationParams const& navigation_params)
{
auto extra_head_contents = R"~~~(
<style>
.zoomable {
cursor: zoom-in;
max-width: 100%;
}
.zoomable.zoomed-in {
cursor: zoom-out;
max-width: none;
}
</style>
<script>
function imageClickEventListener(event) {
let image = event.target;
if (image.classList.contains("zoomable")) {
image.classList.toggle("zoomed-in");
}
}
function processImages() {
let images = document.querySelectorAll("img");
let windowWidth = window.innerWidth;
images.forEach((image) => {
if (image.naturalWidth > windowWidth) {
image.classList.add("zoomable");
} else {
image.classList.remove("zoomable");
image.classList.remove("zoomed-in");
}
image.addEventListener("click", imageClickEventListener);
});
}
document.addEventListener("load", () => {
processImages();
});
window.addEventListener("resize", () => {
processImages();
});
</script>
)~~~"sv;
return create_document_for_inline_content(navigation_params.navigable.ptr(), navigation_params.id, [&](DOM::Document& document) {
auto& realm = document.realm();
auto process_body = JS::create_heap_function(realm.heap(), [&document, url = navigation_params.response->url().value(), extra_head_contents](ByteBuffer data) {
auto markdown_document = Markdown::Document::parse(data);
if (!markdown_document)
return;
auto parser = HTML::HTMLParser::create(document, markdown_document->render_to_html(extra_head_contents), "utf-8"sv);
parser->run(url);
});
auto process_body_error = JS::create_heap_function(realm.heap(), [](JS::Value) {
dbgln("FIXME: Load html page with an error if read of body failed.");
});
navigation_params.response->body()->fully_read(
realm,
process_body,
process_body_error,
JS::NonnullGCPtr { realm.global_object() });
});
}
bool build_xml_document(DOM::Document& document, ByteBuffer const& data, Optional<String> content_encoding)
{
Optional<TextCodec::Decoder&> decoder;
@ -533,8 +464,6 @@ JS::GCPtr<DOM::Document> load_document(HTML::NavigationParams const& navigation_
// native rendering of the content or an error message because the specified type is not supported, then
// return the result of creating a document for inline content that doesn't have a DOM given navigationParams's
// navigable, navigationParams's id, and navigationParams's navigation timing type.
if (type.essence() == "text/markdown"sv)
return load_markdown_document(navigation_params).release_value_but_fixme_should_propagate_errors();
// FIXME: 4. Otherwise, the document's type is such that the resource will not affect navigationParams's navigable,
// e.g., because the resource is to be handed to an external application or because it is an unknown type