From 3514e9c0206c02da294c97c61c7bf675ec8fbc0e Mon Sep 17 00:00:00 2001 From: gravel Date: Sat, 28 Dec 2024 19:09:45 +0000 Subject: [PATCH] feat: render about page from readme --- README.md | 2 +- output/css/common-dark.css | 4 +- php/utils/markdown.php | 297 +++++++++++++++++++++++++++++++++++++ php/utils/utils.php | 10 ++ sites/about/index.php | 64 +++----- 5 files changed, 329 insertions(+), 48 deletions(-) create mode 100644 php/utils/markdown.php diff --git a/README.md b/README.md index 6fc834b..57d25bc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-updating list of Session Communities +# [Self-updating list of Session Communities](https://sessioncommunities.online) ## What is Session? diff --git a/output/css/common-dark.css b/output/css/common-dark.css index dc49d25..f48524c 100644 --- a/output/css/common-dark.css +++ b/output/css/common-dark.css @@ -5,7 +5,7 @@ html { color: var(--primary-color); background-color: var(--secondary-color); font-size: clamp(16px, 2vw, 24px); width: 100%; } body { margin: 0; padding: 2rem; width: 100%; line-height: 1.5; } a { color: hsl(210, 100%, 60%); } -pre, .code { background-color: var(--secondary-color-shaded); font-size: 0.9rem; white-space: break-spaces; overflow: auto; } +pre, code, .code { background-color: var(--secondary-color-shaded); font-size: 0.9rem; white-space: break-spaces; overflow: auto; } pre { padding: 1em; } -.code { padding: 0.1em 0.25em; } +code, .code { padding: 0.1em 0.25em; } .hidden { display: none; } diff --git a/php/utils/markdown.php b/php/utils/markdown.php new file mode 100644 index 0000000..4611b8c --- /dev/null +++ b/php/utils/markdown.php @@ -0,0 +1,297 @@ +readLine($line); + } + + $builder->emit(); +} + +class MarkdownDocumentParser { + /** + * @var MarkdownEntity[] $stack + */ + private array $stack; + private string $document; + + public function __construct() { + $this->document = ""; + $this->stack = []; + } + + public function emit() { + if ($this->topTypeIs('p')) { + $this->pop(); + } + + $stack_size = count($this->stack); + + if ($stack_size > 0) { + log_warning("Stack has $stack_size markdown entities"); + } + + while (count($this->stack) > 0) { + $this->pop(); + } + + echo $this->document; + } + + public function readLine(string $line) { + $this->readStartOfLineEntity($line); + + $this->readEntitiesFrom($line); + + $this->popOneLiners(); + } + + private function readEntitiesFrom(string &$line) { + $count = 0; + while (strlen($line) > 0) { + $this->readEntityFrom($line); + $count += 1; + if ($count > 5000) { + throw new LogicException("Too many entities on line"); + } + } + } + + private function peek(): ?MarkdownEntity { + return $this->stack[count($this->stack) - 1] ?? null; + } + + private function push(string $type, array $props = []) { + $node = new MarkdownEntity($type); + foreach ($props as $key => $val) { + $node->setProp($key, $val); + } + $this->document .= $node->renderStart(); + array_push($this->stack, $node); + } + + private function pop() { + /** + * @var MarkdownEntity $node + */ + $node = array_pop($this->stack); + $this->document .= $node->renderRest(); + } + + private function topType(): ?string { + $top = $this->peek(); + if ($top !== null) { + return $top->type; + } + return null; + } + + private function topTypeIs(string... $matches): bool { + return in_array($this->topType(), $matches); + } + + private function countOf(string $type): int { + $count = 0; + foreach ($this->stack as $node) { + if ($node->type == $type) { + $count += 1; + } + } + return $count; + } + + public function readStartOfLineEntity(string &$line) { + if (strlen(trim($line)) == 0) { + if ($this->topTypeIs('p')) { + $this->pop(); + } + } else if (remove_prefix($line, "#### ")) { + $this->push('h4', [ 'docpos' => strlen($this->document) ]); + } else if (remove_prefix($line, "### ")) { + $this->push('h3', [ 'docpos' => strlen($this->document) ]); + } else if (remove_prefix($line, "## ")) { + $this->push('h2', [ 'docpos' => strlen($this->document) ]); + } else if (remove_prefix($line, "# ")) { + $this->push('h1', [ 'docpos' => strlen($this->document) ]); + } else if ($this->topTypeIs('li', 'ul', 'p') && $this->countOf('li') > 0) { + do { + $indent = str_repeat(" ", 2 * $this->countOf("li")); + + if (remove_prefix($line, "{$indent}- ")) { + if ($this->topTypeIs('ul')) { + $this->push('li'); + break; + } else if ($this->topTypeIs('li')) { + // one more indent satisfied in this case + $this->push('ul'); + $this->push('li'); + break; + } + } else if (!$this->topTypeIs('p') && remove_prefix($line, "{$indent}")) { + if ($this->topTypeIs('li')) { + $this->push('p'); + break; + } else if ($this->topTypeIs('ul')) { + $this->pop(); + $this->push('p'); + break; + } + } + + $this->pop(); + } while ($this->topTypeIs('li', 'ul', 'p')); + } else if (remove_prefix($line, "- ")) { + $this->push('ul'); + $this->push('li'); + } else if (!$this->topTypeIs('p')) { + $this->push('p'); + } + } + + public function popOneLiners() { + while ($this->topTypeIs('h1', 'h2', 'h3', 'h4', 'h5', 'h6')) { + // Need to retroactively add heading id for anchors + $start = (int) $this->peek()->getProp("docpos"); + $taglen = strlen($this->peek()->renderStart()); + $contents = substr($this->document, $start + $taglen); + $contents = strip_tags($contents); + $slug = trim(preg_replace("/[^a-z]/", '-', trim(strtolower($contents))), '-'); + $this->document = substr_replace($this->document, " id=\"$slug\"", $start + $taglen - 1, 0); + $this->pop(); + } + } + + public function readEntityFrom(string &$line) { + if (str_starts_with($line, "[")) { + $matches = []; + if (preg_match('/^\[(.*?[^\\\\])\]\((.+?)\)/', $line, $matches)) { + $this->push('a', [ 'href' => $matches[2] ]); + $contents = str_replace("\\]", "]", $matches[1]); + $this->readEntitiesFrom($contents); + $this->pop(); + remove_prefix($line, $matches[0]); + return; + } + } + if (str_starts_with($line, "<")) { + $matches = []; + if (preg_match('/^\<(.*?[^\\\\])\>/', $line, $matches)) { + $contents = str_replace("\\>", ">", $matches[1]); + $this->push('a', [ 'href' => $contents ]); + $this->readEntitiesFrom($contents); + $this->pop(); + remove_prefix($line, $matches[0]); + return; + } + } + if (str_starts_with($line, "**")) { + $matches = []; + if (preg_match('/\*\*(.*?[^\\\\])\*\*/', $line, $matches)) { + $this->push('strong'); + $contents = str_replace("\\**", "**", $matches[1]); + $this->readEntitiesFrom($contents); + $this->pop(); + remove_prefix($line, $matches[0]); + return; + } + } + if (str_starts_with($line, "*")) { + $matches = []; + if (preg_match('/\*(.*?[^\\\\])\*/', $line, $matches)) { + $this->push('em'); + $contents = str_replace("\\*", "*", $matches[1]); + $this->readEntitiesFrom($contents); + $this->pop(); + remove_prefix($line, $matches[0]); + return; + } + } + if (str_starts_with($line, "`")) { + $matches = []; + if (preg_match('/\`(.*?[^\\\\])\`/', $line, $matches)) { + $this->push('code'); + $contents = str_replace("\\`", "`", $matches[1]); + $this->readEntitiesFrom($contents); + $this->pop(); + remove_prefix($line, $matches[0]); + return; + } + } + // consume word + $matches = []; + if (preg_match('/^\s*\S+\s*/', $line, $matches)) { + if (!str_ends_with($this->document, " ") + && !str_ends_with($this->document, ">") + && !str_starts_with($matches[0], " ")) { + $this->document .= " "; + } + $this->document .= $matches[0]; + remove_prefix($line, $matches[0]); + return; + } + log_value($this, LoggingVerbosity::Warning); + log_value($line, LoggingVerbosity::Warning); + throw new LogicException("Could not parse any entity"); + } +} + +class MarkdownEntity { + public function __construct(string $type) { + $this->type = $type; + $this->props = []; + } + + public function setProp(string $prop, string $value) { + $this->props[$prop] = $value; + } + + public function appendProp(string $prop, string $value) { + $this->props[$prop] ??= ''; + $this->props[$prop] .= $value; + } + + public function getProp(string $prop): string { + return $this->props[$prop]; + } + + public string $type; + private array $props; + + public function renderStart(): string { + global $REPOSITORY_CANONICAL_URL_FILES; + + $type = $this->type; + switch ($type) { + case 'a': + $href = $this->getProp('href'); + if (!str_starts_with($href, "#") && !str_contains($href, "://")) { + $href = "$REPOSITORY_CANONICAL_URL_FILES/$href"; + } + return "\n"; + default: return "\n<$type>"; + } + } + + public function renderRest(): string { + $type = $this->type; + switch ($type) { + default: return ""; + } + } +} diff --git a/php/utils/utils.php b/php/utils/utils.php index c8f0f9c..f0cb97f 100644 --- a/php/utils/utils.php +++ b/php/utils/utils.php @@ -210,4 +210,14 @@ $length = $end - $start; if ($length < 0) return ""; else return substr($string, $start, $length); } + + function remove_prefix(string &$haystack, string $needle): bool { + if (!str_starts_with($haystack, $needle)) { + return false; + } + + $haystack = substr_replace($haystack, "", 0, strlen($needle)); + + return true; + } ?> diff --git a/sites/about/index.php b/sites/about/index.php index 391b894..f1e38fb 100644 --- a/sites/about/index.php +++ b/sites/about/index.php @@ -1,63 +1,37 @@ + - + - + About — sessioncommunities.online + ?>This web project crawls known sources of Session Communities, and displays information about them as a static HTML page."> + -

About our list of Session Communities

-

What is Session?

-

Session is a private messaging app that protects your meta-data, -encrypts your communications, and makes sure your messaging activities -leave no digital trail behind.

-

What are Session Communities? What does this site do?

-

Session Communities are public chatrooms accessible from within Session Messenger. -This web project crawls known sources of Session Communities, and -displays information about them as a static HTML page.

-

How do I get my Community listed?

-

Answer: Submit your Community to one of our upstream sources, or better yet, create another page with links to your favorite Communities for more redundancy.

-

Then, read our recommendations for Community operators.

-

Which sources do you crawl?

-

Communities displayed come from a variety of sources:

- -Have a list we don't know about? Shoot us a message. -

Contributing

-

See CONTRIBUTING.md for information about running the project and contributing code, or languages/README.md to help add language labels for Communities.

-

Policy

-

We require that content posted in Communities adheres to the Content Policy laid out in the Session Terms of Service. Additionally, we may consider Communities unsuitable for display if their content is in breach of copyright and/or encourages or instructs drug use.

-

If Communities explicitly accept such content or fail to moderate such content, we may display these Communities only to users who have disabled JavaScript in their browser, or to none at all. Therefore, it is crucial your Community has adequate coverage by moderators.

-

For safety reasons, we may also hide Communities intended for testing. We appreciate contact with and the continued vigilance of server operators to prevent any issues from arising that would lead to us de-listing your Community.

-

Contact us

-

To report issues or make suggestions, file an issue on our repository or visit the Web Development Community on caliban.org. Be sure to include proper reasoning with your suggestion.

-

In case your issue cannot be resolved publicly, contact gravel on Session as gravel (Registered Session ID).

- - + + + + +