Compare commits

...

22 Commits

Author SHA1 Message Date
Michael Ochmann 0cc7b0dd45 added changelog for v1.2.0 3 years ago
Michael Ochmann c7dcd85cce cleaned up index.php 3 years ago
Michael Ochmann efa8516a67 fixed typos in readme 3 years ago
Michael Ochmann 507e1e4d26 added tests for headings 3 years ago
Michael Ochmann e6ba398b14 Merge pull request 'Paragraph annotations' (#2) from feature/paragraph-annotations into development 3 years ago
Michael Ochmann e37050c3f4 added tests to check for annotations breaking things 3 years ago
Michael Ochmann 496034258a fixed annoations breaking headings 3 years ago
Michael Ochmann 4e1e44a41b added braces to escapable characters 3 years ago
Michael Ochmann b71dd1d440 added annotation example to readme 3 years ago
Michael Ochmann e841ca30ad Merge branch 'development' into feature/paragraph-annotations 3 years ago
Michael Ochmann 1aa376d6ec fixed bug in parser where bold text on single line was parsed as list 3 years ago
Michael Ochmann 9d407c3706 now parsing `id` and `class` annotations on paragraphs 3 years ago
Michael Ochmann 6100cf0f6b added `LBRACE` and `RBRACE` tokens to lexer 3 years ago
Michael Ochmann 499b2b6fe4 fixed annotation test 3 years ago
Michael Ochmann 8408d735dc Merge branch 'development' into feature/paragraph-annotations 3 years ago
Michael Ochmann d6f4545325 added test for paragraph annotations 3 years ago
Michael Ochmann 39307e24e9 fixed test for codeblocks 3 years ago
Michael Ochmann 08ea243729 added usage example 3 years ago
Michael Ochmann 1bca2ac15c added changelog for new version 3 years ago
Michael Ochmann d7e17e59df bump version 3 years ago
Michael Ochmann 289d68fa2f added support for `prism` style code highlighting 3 years ago
Michael Ochmann abb2dbc690 making shure, php 8.1 or higher is required 3 years ago
  1. 17
      CHANGELOG.md
  2. 43
      README.md
  3. 5
      composer.json
  4. 13
      index.php
  5. 8
      src/Lexer.php
  6. 73
      src/Parser.php
  7. 2
      src/Token.php
  8. 70
      tests/AnnotationsTest.php
  9. 4
      tests/CodeBlocksTest.php
  10. 16
      tests/GenericBlocksTest.php
  11. 65
      tests/HeadingsTest.php

@ -1,5 +1,22 @@
# Changelog
## Version 1.2.0
The *"annotation release"*
### Added
* paragraph annotations *(now can set HTML ids and classes with `{.someClass, #someId}` syntax)*
* unit tests for annotation
* unit for headings *(`h1` to `h5`)*
### Removed
* Testing code in `index.php`
## Version 1.1.2
No major changes introduced, getting ready for release
### Changed
* added compatibility for `prism` code highlighting inside code blocks
## Version 1.1.0
No major changes introduced, getting ready for release

@ -33,11 +33,27 @@ Parkdown currently support the following block types:
### Paragraphs
```markdown
A simple paragraph can contain **bold text**, `inline codeblocks` and *italic text*. We can also link [with a direct url][https://google.com] *(i.e. to google)*
or via reference to [a later defined url][massivedynamic], i we so desire.
or via reference to [a later defined url][massivedynamic], if we so desire.
```
A simple paragraph can contain **bold text**, `inline codeblocks` and *italic text*. We can also link [with a direct url](https://google.com) *(i.e. to google)*
or via reference to [a later defined url][massivedynamic], i we so desire.
or via reference to [a later defined url][massivedynamic], if we so desire.
Paragraphs can be annotated with `id` and `class` attributes:
```markdown
Paragraphs can be annotated with ids and classes {.thisIsAClass, .anotherClass, #thisIsAnID}
```
results in
Paragraphs can be annotated with ids and classes {.thisIsAClass, .anotherClass, #thisIsAnID}
```html
<p class="thisIsAClass anotherClass" id="thisIsAnID">
Paragraphs can be annotated with ids and classes
</p>
```
### Images
```markdown
@ -57,14 +73,14 @@ or via reference to [a later defined url][massivedynamic], i we so desire.
> Only two things are infinite,
> the universe and human stupidity,
> i am not totally shure about the universe, though...
> - Albert Einstein
> Albert Einstein
```
> Only two things are infinite,
> the universe and human stupidity,
> i am not totally shure about the universe, though...
> - Albert Einstein
> Albert Einstein
### Code blocks
```markdown
@ -112,6 +128,25 @@ or via reference to [a later defined url][massivedynamic], i we so desire.
[massivedynamic]: https://massivedynamic.eu
## Usage
Simply construct an new `parkdown\Parkdown` object and pass the Markdown source code to it's constructor. The parsed `DOMDocument` or it's `HTML` output can then be retrieved through the `::html()` and `::tree()` member functions.
**Example**
```php
use parkdown\Parkdown;
$source = "
This is a **bold** word in a paragraph.
";
$parser = new Parkdown($source);
$tree = $parser->tree();
print_r($tree);
echo $parser->html();
```
## Testing
Unit tests can be run via `composer`:

@ -1,7 +1,7 @@
{
"name": "massivedynamic/parkdown",
"type": "library",
"version": "1.1.0",
"version": "v1.2.0",
"license": "MIT",
"autoload": {
"psr-4": {
@ -14,6 +14,9 @@
"email": "miko@massivedynamic.eu"
}
],
"require": {
"php" : ">=8.1"
},
"require-dev": {
"phpunit/phpunit": "^9"
},

@ -4,15 +4,24 @@ require __DIR__."/vendor/autoload.php";
$source = file_get_contents(dirname(__FILE__)."/README.md");
//$source = file_get_contents(dirname(__FILE__)."/tests/paragraph.md");
//$source = file_get_contents(dirname(__FILE__)."/tests/list.md");
echo "
<style>
body {
padding: 4rem;
margin: 0;
font-family: sans-serif;
}
img {
max-width: 100%;
}
code {
word-break: break-word;
white-space: break-spaces;
}
table {
width: 100%;
}

@ -104,6 +104,14 @@ class Lexer {
$clearBuffer();
array_push($tokens, new Token(TokenType::COLON, $char));
break;
case '{':
$clearBuffer();
array_push($tokens, new Token(TokenType::LBRACE, $char));
break;
case '}':
$clearBuffer();
array_push($tokens, new Token(TokenType::RBRACE, $char));
break;
default:
$buffer .= $char;
break;

@ -2,6 +2,7 @@
namespace parkdown;
use Attribute;
use DOMDocument;
use DOMElement;
use DOMNode;
@ -11,6 +12,11 @@ enum ListType {
case UNORDERED;
}
class Attributes {
public array $classes = [];
public ?string $id = null;
}
class Parser {
const MAGIC_CHAR = "*";
@ -148,8 +154,8 @@ class Parser {
}
private function parseText($paragraph = false) : array {
$elms = [];
$buffer = "";
$elms = [];
$buffer = "";
$clearBuffer = function() use (&$elms, &$buffer) {
array_push($elms, $this->document->createTextNode($buffer));
@ -164,7 +170,8 @@ class Parser {
TokenType::BACKTICK,
TokenType::ASTERISK,
TokenType::LBRACKET,
TokenType::BANG
TokenType::BANG,
TokenType::LBRACE
])) {
$this->consume()->data; // backslash
$buffer .= $this->consume()->data;
@ -228,6 +235,41 @@ class Parser {
$clearBuffer();
array_push($elms, $elm);
continue;
} elseif ($this->current()->type === TokenType::LBRACE) {
$lbrace = $this->consume();
assert($lbrace->type === TokenType::LBRACE, "expected left brace, got ".$lbrace->type->name);
$content = "";
while ($this->current()->type !== TokenType::EOF &&
$this->current()->type !== TokenType::EOL &&
$this->current()->type !== TokenType::RBRACE) {
$content .= $this->consume()->data;
}
$rbrace = $this->consume();
assert($rbrace->type === TokenType::RBRACE, "expected right brace, got ".$rbrace->type->name);
$attributes = array_map(function($element) {
return trim($element);
}, explode(',', $content));
$obj = new Attributes();
foreach($attributes as $attribute) {
if (!in_array($attribute[0], [".", "#"]))
continue;
switch ($attribute[0]) {
case ".":
array_push($obj->classes, substr($attribute, 1));
break;
case "#":
$obj->id = substr($attribute, 1);
break;
default:
continue 2;
}
}
array_push($elms, $obj);
} else
$buffer .= self::StripBackslashes($this->consume()->data);
}
@ -268,6 +310,7 @@ class Parser {
// then we parse the node content
$elm = $this->document->createElement("li");
foreach ($this->parseText() as $node)
if ($node instanceof DOMNode)
$elm->appendChild($node);
// now we check, if the level of the next line is higher than the current level.
@ -303,10 +346,17 @@ class Parser {
private function buildParagraph(array $elms) : void {
if (count($elms) < 1)
return;
$elm = $this->document->createElement("p");
$i = 0;
foreach ($elms as $node) {
if ($node instanceof Attributes) {
if (count($node->classes) > 0)
$elm->setAttribute("class", join(" ", $node->classes));
if ($node->id)
$elm->setAttribute("id", $node->id);
continue;
}
if ($node->nodeName === "#text" && trim($node->textContent) === "")
continue;
$elm->appendChild($node);
@ -326,7 +376,8 @@ class Parser {
}
$elm = $this->document->createElement("h".$level);
foreach ($this->parseText() as $node)
$elm->appendChild($node);
if ($node instanceof DOMNode)
$elm->appendChild($node);
$this->document->appendChild($elm);
}
@ -344,8 +395,10 @@ class Parser {
$lang = count($lang) > 0 ? trim($lang[0]->data) : null;
$container = $this->document->createElement("pre");
if ($lang)
if ($lang) {
$container->setAttribute("data-lang", $lang);
$container->setAttribute("class", "language-$lang");
}
$buffer = "";
while (!($this->current()->type === TokenType::BACKTICK &&
@ -360,6 +413,10 @@ class Parser {
}
$elm = $this->document->createElement("code", htmlspecialchars($buffer));
if ($lang) {
$elm->setAttribute("data-lang", $lang);
$elm->setAttribute("class", "language-$lang");
}
$container->appendChild($elm);
$this->document->appendChild($container);
$this->consume();
@ -505,6 +562,10 @@ class Parser {
while ($this->current()->type !== TokenType::EOF) {
switch($this->current()->type) {
case TokenType::ASTERISK:
if ($this->next()->type === TokenType::ASTERISK) {
$this->buildParagraph($this->parseText(true));
break;
}
$list = $this->parseList();
$this->document->appendChild($list);
break;

@ -22,6 +22,8 @@ enum TokenType {
case PIPE ;
case GT ;
case TAB ;
case LBRACE ;
case RBRACE ;
}
class Token {

@ -0,0 +1,70 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class AnnotationsTest extends TestCase {
public function testAnnotationsDoNotBreakHeadings() : void {
$source = "
# This is an H1 {.withAClass}
## This is an H2 {.withAClass}
### This is an H3 {.withAClass}
#### This is an H4 {.withAClass}
##### This is an H5 {.withAClass}
";
$target = "
<h1>This is an H1</h1>
<h2>This is an H2</h2>
<h3>This is an H3</h3>
<h4>This is an H4</h4>
<h5>This is an H5</h5>
";
[$source, $result] = createTest($source, $target);
$this->assertEquals($source, $result);
}
public function testAnnotationsDoNotBreakLists() : void {
$source = "
* this is a list
* with annotations {#someID}
1. this is an ordered list
1. with an annotation {.someClass}
";
$target = "
<ul>
<li>this is a list</li>
<li>with annotations</li>
</ul>
<ol>
<li>
this is an ordered list
<ol>
<li>with an annotation</li>
</ol>
</li>
</ol>
";
[$source, $result] = createTest($source, $target);
$this->assertEquals($source, $result);
}
public function testAnnotationsDoNotBreakCodeblocks() : void {
$source = "
```
this is a code block
``` {.someClass}
";
$target = "
<pre>
<code>this is a code block</code>
</pre>
";
[$source, $result] = createTest($source, $target);
$this->assertEquals($source, $result);
}
}

@ -31,8 +31,8 @@ final class CodeBlocksTest extends TestCase {
```
";
$target = "
<pre data-lang=\"php\">
<code>
<pre data-lang=\"php\" class=\"language-php\">
<code data-lang=\"php\" class=\"language-php\">
public function testLanguageAnnotationParsesCorrectly() : bool {
return true;
}

@ -44,4 +44,20 @@ this is a paragraph
[$source, $result] = createTest($source, $target);
$this->assertEquals($source, $result);
}
public function testParagraphAnnotations() : void {
$source = "
this is an annotated paragraph {.classA, .classB}
";
$target = "
<p class=\"classA classB\">
this is an annotated paragraph
</p>
";
[$source, $result] = createTest($source, $target);
$this->assertEquals($source, $result);
}
}

@ -0,0 +1,65 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class HeadingsTest extends TestCase {
public function testH1ParsesCorrectly() : void {
$source = "
# This is an H1
";
$target = "
<h1>This is an H1</h1>
";
[$source, $result] = createTest($source, $target);
$this->assertEquals($source, $result);
}
public function testH2ParsesCorrectly() : void {
$source = "
## This is an H2
";
$target = "
<h2>This is an H2</h2>
";
[$source, $result] = createTest($source, $target);
$this->assertEquals($source, $result);
}
public function testH3ParsesCorrectly() : void {
$source = "
### This is an H3
";
$target = "
<h3>This is an H3</h3>
";
[$source, $result] = createTest($source, $target);
$this->assertEquals($source, $result);
}
public function testH4ParsesCorrectly() : void {
$source = "
#### This is an H4
";
$target = "
<h4>This is an H4</h4>
";
[$source, $result] = createTest($source, $target);
$this->assertEquals($source, $result);
}
public function testH5ParsesCorrectly() : void {
$source = "
##### This is an H5
";
$target = "
<h5>This is an H5</h5>
";
[$source, $result] = createTest($source, $target);
$this->assertEquals($source, $result);
}
}
Loading…
Cancel
Save