├── posts ├── kudos │ ├── kudos.txt │ ├── 2023-08-01-count.txt │ └── 2023-09-01-count.txt ├── moon.jpg ├── moon_by_cc0.jpg ├── 2023-09-01.md └── 2023-08-01.md ├── .gitignore ├── assets ├── css │ ├── custom.css │ ├── commento.css │ ├── normalize.min.css │ └── style.css ├── img │ ├── og.png │ ├── icon.png │ └── author.jpg ├── tools │ ├── feedwriter │ │ ├── RSS2.php │ │ ├── Item.php │ │ └── Feed.php │ ├── FrontMatter.php │ ├── ParsedownExtra.php │ └── parsedown.php └── scripts │ └── webmention.min.js ├── parts ├── footer.php └── header.php ├── page.php ├── robots.txt ├── increment_likes.php ├── nav.php ├── rss.php ├── kudos.php ├── pages └── about-me.md ├── archive.php ├── single.php ├── index.php ├── .htaccess ├── README.md └── config.php /posts/kudos/kudos.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /posts/kudos/2023-08-01-count.txt: -------------------------------------------------------------------------------- 1 | 9 -------------------------------------------------------------------------------- /posts/kudos/2023-09-01-count.txt: -------------------------------------------------------------------------------- 1 | 21 -------------------------------------------------------------------------------- /assets/css/custom.css: -------------------------------------------------------------------------------- 1 | /* Add your custom css here! */ 2 | -------------------------------------------------------------------------------- /posts/moon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nithou/tiny-blog-engine/HEAD/posts/moon.jpg -------------------------------------------------------------------------------- /assets/img/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nithou/tiny-blog-engine/HEAD/assets/img/og.png -------------------------------------------------------------------------------- /assets/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nithou/tiny-blog-engine/HEAD/assets/img/icon.png -------------------------------------------------------------------------------- /assets/img/author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nithou/tiny-blog-engine/HEAD/assets/img/author.jpg -------------------------------------------------------------------------------- /posts/moon_by_cc0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nithou/tiny-blog-engine/HEAD/posts/moon_by_cc0.jpg -------------------------------------------------------------------------------- /parts/footer.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | '.$MOREPOSTS.''; 6 | } 7 | else { 8 | echo ''; 9 | };?> 10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /page.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

6 |
7 | 8 |
9 |
10 |
11 | 12 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | # Disallow AI Agents 2 | User-agent: anthropic-ai 3 | User-agent: AmazonBot 4 | User-agent: Applebot-Extended 5 | User-agent: Bytespider 6 | User-agent: CCBot 7 | User-agent: ChatGPT-User 8 | User-agent: Claudebot 9 | User-agent: Claude-Web 10 | User-agent: cohere-ai 11 | User-agent: Diffbot 12 | User-agent: FacebookBot 13 | User-agent: Google-Extended 14 | User-agent: GPTBot 15 | User-agent: Omgili 16 | User-agent: Omgilibot 17 | User-agent: PerplexityBot 18 | User-agent: YandexAdditionalBot 19 | User-agent: YouBot 20 | Disallow: / 21 | -------------------------------------------------------------------------------- /posts/2023-09-01.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: One World 3 | img: https://nithou.net/blog/posts/img/pastlives.jpg 4 | --- 5 | The view of the earth from the moon fascinated me - a small disk, 240,000 mniles away. It was hard to think that that little thing held so many problems, so many frustrations. Raging nationalistic interests, famines, wars, pestilence don't show from that distance. I'm convinced that some wayward stranger in a space-craft, coming from some other part of the heavens, could look at earth and never know that it was inhabited at all. But the samw wayward stranger would certainly know instinctively that if the earth were inhabited, then the destinies of all who lived on it must inevitably be interwoven and joined. We are one hunk of ground, water, air, clouds, floating around in space. From out there it really is 'one world'. 6 | 7 | -------------------------------------------------------------------------------- /assets/css/commento.css: -------------------------------------------------------------------------------- 1 | .commento-root { 2 | max-width: 50ch; 3 | } 4 | 5 | .commento-root .commento-submit-button { 6 | background: var(--light-primary); 7 | color:var(--light-bg); 8 | } 9 | 10 | .commento-root .commento-login .commento-login-text { 11 | color:var(--light-primary); 12 | transition:all ease-in .25s; 13 | } 14 | 15 | .commento-root:hover .commento-login:hover .commento-login-text:hover { 16 | color:var(--light-links); 17 | } 18 | 19 | @media (prefers-color-scheme: dark) 20 | { 21 | 22 | .commento-root .commento-login .commento-login-text { 23 | color:var(--dark-primary); 24 | } 25 | 26 | .commento-root:hover .commento-login:hover .commento-login-text:hover { 27 | color:var(--dark-links); 28 | } 29 | 30 | .commento-root .commento-submit-button { 31 | background: var(--dark-primary); 32 | color:var(--dark-bg); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /increment_likes.php: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /nav.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/tools/feedwriter/RSS2.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This file is part of the "Universal Feed Writer" project. 8 | * 9 | * This program is free software: you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * This program is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | * GNU General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public License 20 | * along with this program. If not, see . 21 | */ 22 | 23 | /** 24 | * Wrapper for creating RSS2 feeds 25 | * 26 | * @package UniversalFeedWriter 27 | */ 28 | class RSS2 extends Feed 29 | { 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function __construct() 34 | { 35 | parent::__construct(Feed::RSS2); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /assets/css/normalize.min.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */button,hr,input{overflow:visible}progress,sub,sup{vertical-align:baseline}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}details,main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{padding:.35em .75em .625em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none} -------------------------------------------------------------------------------- /rss.php: -------------------------------------------------------------------------------- 1 | setTitle($BLOG_TITLE) 18 | ->setLink($BLOG_LINK) 19 | ->setDescription($BLOG_DESCRIPTION) 20 | ->setChannelElement('language', $LANG) 21 | ->setSelfLink($BLOG_LINK . 'rss.php') 22 | ->setAtomLink($BLOG_LINK . 'rss.php', 'hub') 23 | ->addGenerator() 24 | ->setImage($BLOG_TITLE, $BLOG_LINK, $BLOG_LINK . 'assets/img/og.png'); 25 | 26 | $files = glob("posts/*.md"); 27 | rsort($files); 28 | 29 | foreach ($files as $postFile) { 30 | $link_id = pathinfo($postFile, PATHINFO_FILENAME); 31 | $frontmatter = new FrontMatter($postFile); 32 | $postContent = $frontmatter->fetchContent(); 33 | $Parsedown = new Parsedown(); 34 | $newItem = $TestFeed->createNewItem(); 35 | $meta = $frontmatter->fetchMeta(); 36 | $title = $meta['title']; 37 | $itemDate = !empty($meta['published_date']) ? $meta['published_date'] : filemtime($postFile); 38 | $content = stristr($postContent, "

"); 39 | $contentFinal = $Parsedown->text($postContent); 40 | 41 | $newItem 42 | ->setTitle($title) 43 | ->setLink($BLOG_LINK . 'single.php?id=' . $link_id) 44 | ->setID($BLOG_LINK . 'single.php?id=' . $link_id) 45 | ->setDate($itemDate) 46 | ->setDescription($contentFinal) 47 | ->setAuthor($BLOG_AUTHOR, $AUTHOR_EMAIL); 48 | 49 | $TestFeed->addItem($newItem); 50 | } 51 | 52 | $TestFeed->printFeed(); 53 | -------------------------------------------------------------------------------- /kudos.php: -------------------------------------------------------------------------------- 1 |

2 | ' . $counter . ''; 17 | ?> 18 | 24 |
25 | 26 | -------------------------------------------------------------------------------- /posts/2023-08-01.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: We chose to go to the moon 3 | img: https://nithou.net/sandbox/posts/moon_by_cc0.jpg 4 | --- 5 | There is no strife, no prejudice, no national conflict in outer space as yet. Its hazards are hostile to us all. Its conquest deserves the best of all mankind, and its opportunity for peaceful cooperation many never come again. But why, some say, the moon? Why choose this as our goal? And they may well ask why climb the highest mountain? Why, 35 years ago, fly the Atlantic? Why does Rice play Texas? 6 | 7 | ![Moon photo by cc0.photo](posts/moon.jpg) 8 | 9 | We choose to go to the moon. We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard, because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one which we intend to win, and the others, too. 10 | 11 | It is for these reasons that I regard the decision last year to shift our efforts in space from low to high gear as among the most important decisions that will be made during my incumbency in the office of the Presidency. 12 | 13 | In the last 24 hours we have seen facilities now being created for the greatest and most complex exploration in man's history. We have felt the ground shake and the air shattered by the testing of a Saturn C-1 booster rocket, many times as powerful as the Atlas which launched John Glenn, generating power equivalent to 10,000 automobiles with their accelerators on the floor. We have seen the site where the F-1 rocket engines, each one as powerful as all eight engines of the Saturn combined, will be clustered together to make the advanced Saturn missile, assembled in a new building to be built at Cape Canaveral as tall as a 48 story structure, as wide as a city block, and as long as two lengths of this field. 14 | 15 | -------------------------------------------------------------------------------- /pages/about-me.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About 3 | --- 4 | Look again at that dot. That's here. That's home. That's us. On it everyone you love, everyone you know, everyone you ever heard of, every human being who ever was, lived out their lives. The aggregate of our joy and suffering, thousands of confident religions, ideologies, and economic doctrines, every hunter and forager, every hero and coward, every creator and destroyer of civilization, every king and peasant, every young couple in love, every mother and father, hopeful child, inventor and explorer, every teacher of morals, every corrupt politician, every "superstar," every "supreme leader," every saint and sinner in the history of our species lived there--on a mote of dust suspended in a sunbeam. 5 | 6 | The Earth is a very small stage in a vast cosmic arena. Think of the rivers of blood spilled by all those generals and emperors so that, in glory and triumph, they could become the momentary masters of a fraction of a dot. Think of the endless cruelties visited by the inhabitants of one corner of this pixel on the scarcely distinguishable inhabitants of some other corner, how frequent their misunderstandings, how eager they are to kill one another, how fervent their hatreds. 7 | 8 | Our posturings, our imagined self-importance, the delusion that we have some privileged position in the Universe, are challenged by this point of pale light. Our planet is a lonely speck in the great enveloping cosmic dark. In our obscurity, in all this vastness, there is no hint that help will come from elsewhere to save us from ourselves. 9 | 10 | The Earth is the only world known so far to harbor life. There is nowhere else, at least in the near future, to which our species could migrate. Visit, yes. Settle, not yet. Like it or not, for the moment the Earth is where we make our stand. 11 | 12 | It has been said that astronomy is a humbling and character-building experience. There is perhaps no better demonstration of the folly of human conceits than this distant image of our tiny world. To me, it underscores our responsibility to deal more kindly with one another, and to preserve and cherish the pale blue dot, the only home we've ever known. -------------------------------------------------------------------------------- /archive.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | fetchMeta() as $key => $value) { 21 | $meta[$key] = $value; 22 | } 23 | 24 | // Get post type 25 | if (!empty($meta['type'])) { 26 | $type = $meta['type']; 27 | } else { 28 | // Fallback to the file's last modification time 29 | $type = "single"; 30 | } 31 | 32 | // Get content & summary 33 | $content= $frontmatter->fetchContent(); 34 | $summary = substr($content, 1, 180); 35 | $Parsedown = new ParsedownExtra(); 36 | 37 | if ($SHOW_SUMMARY === TRUE) { 38 | echo '
'; 39 | echo '

'.$meta['title'].'

'; 40 | echo '
'.$Parsedown->text($summary).'
'; 41 | echo ''; 42 | echo '
'; 43 | $counter++; 44 | } else { 45 | echo '
'; 46 | echo '

'.$meta['title'].'

'; 47 | echo '
'.$Parsedown->text($frontmatter->fetchContent()).'
'; 48 | if (!empty($meta['img'])) {echo '';}; 49 | echo ''; 50 | echo '
'; 51 | }; 52 | }; 53 | ?> 54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /single.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

6 |
7 | 8 | 9 | '; 15 | } 16 | ?> 17 | 18 |
19 | 20 |
21 | 22 | 23 | 26 | 27 | 28 |
29 |
30 | <?php echo $BLOG_AUTHOR; ?> 31 |
32 |

33 |

34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 53 |
54 | 55 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | fetchMeta() as $key => $value) { 21 | $meta[$key] = $value; 22 | } 23 | 24 | // Get post type 25 | if (!empty($meta['type'])) { 26 | $type = $meta['type']; 27 | } else { 28 | // Default to single type when not specified. 29 | $type = "single"; 30 | } 31 | 32 | // Get content & summary 33 | $content= $frontmatter->fetchContent(); 34 | $summary = substr($content, 1, 180); 35 | $Parsedown = new ParsedownExtra(); 36 | if ($counter == $POST_LIMIT) { 37 | break; 38 | } 39 | 40 | if ($SHOW_SUMMARY === TRUE) { 41 | echo '
'; 42 | echo '

'.$meta['title'].'

'; 43 | echo '
'.$Parsedown->text($summary).'
'; 44 | echo ''; 45 | echo '
'; 46 | $counter++; 47 | } else { 48 | echo '
'; 49 | echo '

'.$meta['title'].'

'; 50 | echo '
'.$Parsedown->text($frontmatter->fetchContent()).'
'; 51 | if (!empty($meta['img'])) {echo '';}; 52 | echo ''; 53 | echo '
'; 54 | $counter++; 55 | }; 56 | }; 57 | ?> 58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # MOD_DEFLATE COMPRESSION 2 | SetOutputFilter DEFLATE 3 | AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml application/x-javascript application/x-httpd-php 4 | 5 | # The lines above enable GZIP compression for certain file types, which helps reduce page load times by compressing data before sending it to the browser. 6 | 7 | # For incompatible browsers 8 | BrowserMatch ^Mozilla/4 gzip-only-text/html 9 | BrowserMatch ^Mozilla/4\.0[678] no-gzip 10 | BrowserMatch \bMSIE !no-gzip !gzip-only-text/html 11 | BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html 12 | 13 | # These lines specify how to handle compression for different user agents (browsers). 14 | 15 | # Do not cache if these files are already cached 16 | SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip 17 | 18 | # These lines prevent re-compression of already compressed image files to avoid double compression. 19 | 20 | # Proxies must serve the correct content 21 | Header append Vary User-Agent env=!dont-vary 22 | 23 | # BEGIN Expire headers 24 | 25 | ExpiresActive On 26 | ExpiresDefault "access plus 7200 seconds" 27 | 28 | 29 | # The lines above set expiration headers for various file types, which can help browsers cache static content. 30 | 31 | # END Expire headers 32 | 33 | # BEGIN Cache-Control Headers 34 | 35 | 36 | Header set Cache-Control "max-age=31536000, public" 37 | 38 | 39 | Header set Cache-Control "max-age=31536000, public" 40 | 41 | 42 | Header set Cache-Control "max-age=31536000, private" 43 | 44 | 45 | Header set Cache-Control "max-age=7200, public" 46 | 47 | 48 | # The lines above configure cache control headers for specific file types, instructing browsers to cache them for a certain duration. 49 | 50 | # Disable caching for scripts and other dynamic files 51 | 52 | Header unset Cache-Control 53 | 54 | 55 | 56 | # The lines above prevent caching of certain script and dynamic files, ensuring they are fetched fresh from the server each time. 57 | 58 | # END Cache-Control Headers 59 | 60 | # KILL THEM ETAGS 61 | Header unset ETag 62 | FileETag none 63 | 64 | # These lines disable ETags and FileETags, which can reduce unnecessary HTTP requests. 65 | 66 | # Forbid AI Crawlers 67 | RewriteEngine On 68 | RewriteCond %{HTTP_USER_AGENT} (anthropic-ai|AmazonBot|Applebot-Extended|Bytespider|CCBot|ChatGPT-User|Claudebot|Claude-Web|cohere-ai|Diffbot|FacebookBot|Google-Extended|GPTBot|Omgili|Omgilibot|PerplexityBot|YandexAdditionalBot|YouBot) [NC] 69 | RewriteRule ^ – [F] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny Blog Engine 2 | 3 | The Tiny Blog Engine is a lightweight and highly customizable blog engine that empowers you to create and modify your own blogging platform with ease. 4 | 5 | It relies on [Markdown](https://www.markdownguide.org/basic-syntax/) files for your posts and pages! 6 | 7 | - **Lightweight**: The entire engine is only 230kb in size. 8 | - **Dark & Light Mode**: Enjoy the convenience of built-in dark and light modes. 9 | - **Customization**: Easily customize the colors, fonts, language to match your preferences. 10 | - **Simple Deployment**: Requires only FTP access for deployment and posting (and a server that supports PHP, should be easy to find in 2023) 11 | - **Comments & Interactions**: See [the documentation]([https://github.com/nithou/tiny-blog-engine/blob/main/README.md#comment--interactions](https://github.com/nithou/tiny-blog-engine/wiki/Comments-&-Reactions)) 12 | - **RSS2 Support**: Keeps your readers updated with RSS2 feed compatibility. 13 | - **GDPR Compliant**: The engine respects your privacy by not relying on external dependencies. 14 | 15 | All the informations are [available on the Wiki](https://github.com/nithou/tiny-blog-engine/wiki)! 16 | 17 | ![Example showcasing dark and light mode](https://github.com/nithou/tiny-blog-engine/blob/main/assets/img/og.png) 18 | 19 | Check out a [live example here](https://nithou.net/sandbox/) or see a modified version on [my personal blog](https://nithou.net/blog/). 20 | 21 | ## License 22 | 23 | This is free and unencumbered software released into the public domain. 24 | 25 | Anyone is free to copy, modify, publish, use, compile, sell, or 26 | distribute this software, either in source code form or as a compiled 27 | binary, for any purpose, commercial or non-commercial, and by any 28 | means. 29 | 30 | In jurisdictions that recognize copyright laws, the author or authors 31 | of this software dedicate any and all copyright interest in the 32 | software to the public domain. We make this dedication for the benefit 33 | of the public at large and to the detriment of our heirs and 34 | successors. We intend this dedication to be an overt act of 35 | relinquishment in perpetuity of all present and future rights to this 36 | software under copyright law. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 39 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 40 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 41 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 42 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 43 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 44 | OTHER DEALINGS IN THE SOFTWARE. 45 | 46 | For more information, please refer to [Unlicense](https://unlicense.org) 47 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 51 | -------------------------------------------------------------------------------- /assets/scripts/webmention.min.js: -------------------------------------------------------------------------------- 1 | // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt 2 | !function(){"use strict";window.i18next=window.i18next||{t:function(n){return n}};const n=window.i18next.t.bind(window.i18next);function t(n,t){return document.currentScript.getAttribute("data-"+n)||t}const e=t("page-url",window.location.href.replace(/#.*$/,"")),o=t("add-urls",void 0),r=t("id","webmentions"),s=t("wordcount"),i=t("max-webmentions",30),l=t("prevent-spoofing")?"wm-source":"url",c=t("sort-by","published"),a=t("sort-dir","up"),u=t("comments-are-reactions"),p={"in-reply-to":n("replied"),"like-of":n("liked"),"repost-of":n("reposted"),"bookmark-of":n("bookmarked"),"mention-of":n("mentioned"),rsvp:n("RSVPed"),"follow-of":n("followed")},f={"in-reply-to":"💬","like-of":"❤️","repost-of":"🔄","bookmark-of":"⭐️","mention-of":"💬",rsvp:"📅","follow-of":"🐜"},d={yes:"✅",no:"❌",interested:"💡",maybe:"💭"};function m(n){return n.replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}function h(t,e){const o=m(t.author?.name||t.url.split("/")[2]);let r=p[t["wm-property"]]||n("reacted");!e&&t.content&&t.content.text&&(r+=": "+g(t));let s="";t.author&&t.author.photo&&(s=`\n \n `);let i="";return t.rsvp&&d[t.rsvp]&&(i=`${d[t.rsvp]}`),`\n \n ${s}\n ${f[t["wm-property"]]||"💥"}\n ${i}\n \n `}function w(n){return n.substr(n.indexOf("//"))}function $(n){const t=[],e={};return n.forEach((function(n){const o=w(n.url);e[o]||(t.push(n),e[o]=!0)})),t}function g(n){let t=m(n.content.text);if(s){let n=t.replace(/\s+/g," ").split(" ",s+1);n.length>s&&(n[s-1]+="…",n=n.slice(0,s),t=n.join(" "))}return t}window.addEventListener("load",(async function(){const t=document.getElementById(r);if(!t)return;const s=[w(e)];o&&o.split("|").forEach((function(n){s.push(w(n))}));let p=`https://webmention.io/api/mentions.jf2?per-page=${i}&sort-by=${c}&sort-dir=${a}`;s.forEach((function(n){p+=`&target[]=${encodeURIComponent("http:"+n)}&target[]=${encodeURIComponent("https:"+n)}`}));let f={};try{const n=await window.fetch(p);n.status>=200&&n.status<300?f=await n.json():(console.error("Could not parse response"),new Error(n.statusText))}catch(n){console.error("Request failed",n)}let d=[];const b=[];u&&(d=b);const y={"in-reply-to":d,"like-of":b,"repost-of":b,"bookmark-of":b,"follow-of":b,"mention-of":d,rsvp:d};f.children.forEach((function(n){const t=y[n["wm-property"]];t&&t.push(n)}));let k="";d.length>0&&d!==b&&(k=function(t){return`\n

${n("Responses")}

\n
    ${t.map((t=>{const e=h(t,!0);let o=m(t.url.split("/")[2]);t.author&&t.author.name&&(o=m(t.author.name));const r=`${o}`;let s="name",i=`(${n("mention")})`;return t.name?(s="name",i=t.name):t.content&&t.content.text&&(s="text",i=g(t)),`
  • ${e} ${r} ${i}
  • `})).join("")}
\n `}($(d)));let x="";var v;b.length>0&&(v=$(b),x=`\n

${n("Reactions")}

\n
    ${v.map((n=>h(n))).join("")}
\n `),t.innerHTML=`${k}${x}`}))}(); 3 | // @license-end -------------------------------------------------------------------------------- /parts/header.php: -------------------------------------------------------------------------------- 1 | 2 | $LIGHT_BACKGROUND, 7 | '--light-primary' => $LIGHT_TEXT, 8 | '--light-links' => $LIGHT_LINKS, 9 | '--dark-bg' => $DARK_BACKGROUND, 10 | '--dark-primary' => $DARK_TEXT, 11 | '--dark-links' => $DARK_LINKS, 12 | '--titles-font' => $TITLES, 13 | '--texts-font' => $TEXTS, 14 | ]; 15 | ?> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | fetchMeta(); 56 | $title = $meta['title']; 57 | $pubDate = $meta['published_date']; 58 | $titleHeader = $meta['title'] . " | " . $BLOG_TITLE; 59 | 60 | // Parse content using Parsedown 61 | $content = $Parsedown->text($frontmatter->fetchContent()); 62 | 63 | // Get description from the metas or fall back to the first line 64 | if (!empty($meta['description'])) { 65 | $summary = $meta['description']; 66 | } else { 67 | // Fallback to the file's last modification time 68 | $summary = implode(".", array_slice(explode(".", $frontmatter->fetchContent()), 0, 1)) . "..."; 69 | } 70 | 71 | // Set image if available, fallback to default 72 | $img = !empty($meta['img']) ? $meta['img'] : $BLOG_LINK . 'assets/img/og.png'; 73 | 74 | return compact('title', 'titleHeader', 'pubDate', 'content', 'summary', 'img'); 75 | } 76 | 77 | // Determine if it's a post or page 78 | if (stripos($_SERVER['REQUEST_URI'], 'single.php') !== false) { 79 | $id = $_GET['id']; 80 | extract(processFrontMatter('posts', $id, $Parsedown, $BLOG_TITLE, $BLOG_LINK)); 81 | } elseif (stripos($_SERVER['REQUEST_URI'], 'page.php') !== false) { 82 | $id = $_GET['id']; 83 | extract(processFrontMatter('pages', $id, $Parsedown, $BLOG_TITLE, $BLOG_LINK)); 84 | } 85 | ?> 86 | 87 | 88 | <?php echo $titleHeader; ?> 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 103 | 104 | 105 | 106 | 107 |
108 |

109 |

110 |
111 |
112 | 113 | 114 | -------------------------------------------------------------------------------- /assets/tools/FrontMatter.php: -------------------------------------------------------------------------------- 1 | Read($file); 27 | $this->yaml_separator = "---\n"; 28 | $fm = $this->FrontMatter($file); 29 | 30 | foreach($fm as $key => $value) 31 | { 32 | $this->data[$key] = $value; 33 | } 34 | } 35 | 36 | /** 37 | * fetch method returns the value of a given key 38 | * @return string $value The value for a given key 39 | */ 40 | public function fetch($key) 41 | { 42 | return $this->data[$key]; 43 | } 44 | 45 | public function fetchAll() { 46 | 47 | return $this->data; 48 | } 49 | 50 | public function fetchContent() { 51 | 52 | return $this->data['content']; 53 | } 54 | 55 | public function fetchMeta() { 56 | $meta = $this->data; 57 | unset($meta['content']); 58 | return $meta; 59 | } 60 | 61 | /** 62 | * keyExists method Checks to see if a key exists 63 | * @return bool 64 | */ 65 | public function keyExists($key) 66 | { 67 | #return (isset($this->data[$key])) ? true : false; # Isset Version 68 | return array_key_exists($key, $this->data); # array_key_exists version 69 | } 70 | 71 | /** 72 | * fetchKeys method returns an array of all meta data without the content 73 | * @return [array] collection of all meta keys provided to FrontMatter 74 | */ 75 | public function fetchKeys() 76 | { 77 | # Cache the keys so we don't edit the native object data 78 | $keys = $this->data; 79 | 80 | # Remove $data[content] from the keys so we only have the meta data 81 | array_pop($keys); 82 | 83 | return $keys; 84 | } 85 | 86 | /** 87 | * FrontMatter method, rturns all the variables from a YAML Frontmatter input 88 | * @param string $input The input string 89 | * @return array $final returns all variables in an array 90 | */ 91 | function FrontMatter($input) 92 | { 93 | if (!$this->startsWith($input, $this->yaml_separator)) 94 | { 95 | # No front matter 96 | # Store Content in Final array 97 | $final['content'] = $input; 98 | # Return Final array 99 | return $final; 100 | } 101 | 102 | # Explode Seperators. At most, make three pieces out of the input file 103 | $document = explode($this->yaml_separator,$input, 3); 104 | 105 | switch( sizeof($document) ) 106 | { 107 | case 0: 108 | case 1: 109 | // Empty document 110 | $front_matter = ""; 111 | $content = ""; 112 | break; 113 | case 2: 114 | # Only front matter given 115 | $front_matter = $document[1]; 116 | $content = ""; 117 | break; 118 | default: 119 | # Normal document 120 | $front_matter = $document[1]; 121 | $content = $document[2]; 122 | } 123 | 124 | # Split lines in front matter to get variables 125 | $front_matter = explode("\n",$front_matter); 126 | foreach($front_matter as $variable) 127 | { 128 | # Explode so we can see both key and value 129 | $var = explode(": ",$variable,2); 130 | 131 | # Ignore empty lines 132 | if (count($var) > 1) { 133 | 134 | # Store Key and Value 135 | $key = $var[0]; 136 | $val = $var[1]; 137 | 138 | # Store Content in Final array 139 | $final[$key] = $val; 140 | } 141 | } 142 | 143 | # Store Content in Final array 144 | $final['content'] = $content; 145 | 146 | # Return Final array 147 | return $final; 148 | } 149 | 150 | /** 151 | * A convenience wrapper around strpos to check the start of a string 152 | * From http://stackoverflow.com/a/860509/270334 153 | * @return boolean $startswithneedle string starts with $needle 154 | */ 155 | private function startsWith($haystack,$needle,$case=true) 156 | { 157 | if($case) 158 | return strpos($haystack, $needle, 0) === 0; 159 | return stripos($haystack, $needle, 0) === 0; 160 | } 161 | 162 | /** 163 | * Read Method, Read file and returns it's contents 164 | * @return string $data returned data 165 | */ 166 | protected function Read($file) 167 | { 168 | # Open File 169 | $fh = fopen($file, 'r'); 170 | 171 | $fileSize = filesize($file); 172 | 173 | if(!empty($fileSize)) 174 | { 175 | # Read Data 176 | $data = fread($fh, $fileSize); 177 | 178 | # Fix Data Stream to be the exact same format as PHP's strings 179 | $data = str_replace(array("\r\n", "\r", "\n"), "\n", $data); 180 | } 181 | else 182 | { 183 | $data = ''; 184 | } 185 | 186 | # Close File 187 | fclose($fh); 188 | 189 | # Return Data 190 | return $data; 191 | } 192 | } -------------------------------------------------------------------------------- /assets/css/style.css: -------------------------------------------------------------------------------- 1 | /* Global Styles */ 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html:focus-within { 8 | scroll-behavior: smooth; 9 | } 10 | 11 | ::selection { 12 | background-color: hsla(355, 100%, 91%, 0.3); 13 | } 14 | 15 | /* Body Styles */ 16 | 17 | body { 18 | font-family: var(--texts-font); 19 | display: grid; 20 | justify-items: center; 21 | min-height: 100vh; 22 | margin: 0; 23 | padding: 4rem 0 8rem; 24 | color: var(--light-primary); 25 | background-color: var(--light-bg); 26 | -webkit-font-smoothing: antialiased; 27 | -moz-osx-font-smoothing: grayscale; 28 | font-feature-settings: "diga"; 29 | font-variant-ligatures: "discretionary-ligatures"; 30 | } 31 | 32 | p { 33 | padding: 0; 34 | text-align: left; 35 | line-height: 1.5em; 36 | } 37 | 38 | h1, h2, h3 { 39 | font-family: var(--titles-font); 40 | } 41 | 42 | /* Header Styles */ 43 | 44 | header { 45 | max-width: 95%; 46 | margin: auto; 47 | } 48 | 49 | header h1 { 50 | font-variant-ligatures: normal; 51 | color: var(--light-primary); 52 | font-size: 2rem; 53 | margin-bottom: 0; 54 | text-align: center; 55 | } 56 | 57 | header h2 { 58 | font-size: 1.3rem; 59 | color: var(--light-primary); 60 | margin: 0 0 1rem; 61 | text-align: center; 62 | font-weight: 400; 63 | } 64 | 65 | /* Link Styles */ 66 | 67 | a { 68 | position: relative; 69 | color: inherit; 70 | transition: color 0.5s ease-in; 71 | } 72 | 73 | a:hover { 74 | color: var(--light-links); 75 | } 76 | 77 | /* Main Section Styles */ 78 | 79 | main { 80 | max-width: 90%; 81 | margin: auto; 82 | } 83 | 84 | /* Titles Scales */ 85 | 86 | .e-content > h1 { 87 | font-size: 1.3rem; 88 | } 89 | 90 | /* Ensure Fluid Elements Inside Container */ 91 | 92 | .h-entry img, .h-entry iframe, .h-entry table, .h-entry object, .h-entry code, .h-entry pre { 93 | max-width: 100%; 94 | display: block; 95 | } 96 | 97 | .h-entry pre, .h-entry code { 98 | overflow-x: scroll; 99 | overflow-wrap: break-word; 100 | } 101 | 102 | /* Display for Code Blocks */ 103 | 104 | main pre { 105 | background-color: rgba(0, 0, 0, 0.2); 106 | white-space: pre-wrap; 107 | } 108 | 109 | main pre code { 110 | padding: 0.4rem; 111 | } 112 | 113 | /* About Block Styles */ 114 | 115 | .about-me { 116 | max-width: 50ch; 117 | display: flex; 118 | } 119 | 120 | hr { 121 | color: rgba(255, 255, 255, 0.1); 122 | margin-bottom: 1.5rem; 123 | } 124 | 125 | .about-me { 126 | margin-top: 1.5rem; 127 | } 128 | 129 | .about-me .author-image { 130 | float: left; 131 | background-color: white; 132 | border-radius: 100px; 133 | margin-right: 2rem; 134 | display: column; 135 | } 136 | 137 | .about-me .author-infos { 138 | display: column; 139 | } 140 | 141 | .about-me .author-infos h3 { 142 | margin-top: 0; 143 | } 144 | 145 | .about-me .author-image::after { 146 | content: ""; 147 | display: table; 148 | clear: both; 149 | } 150 | 151 | /* Navigation Styles */ 152 | 153 | nav { 154 | display: block; 155 | text-align: center; 156 | font-size: 1rem; 157 | } 158 | 159 | nav > span { 160 | display: block; 161 | margin-bottom: 1rem; 162 | } 163 | 164 | nav a { 165 | text-decoration: none; 166 | } 167 | 168 | nav a:hover, 169 | nav a:active { 170 | text-decoration: underline; 171 | } 172 | 173 | nav .block { 174 | margin: 1em 0; 175 | display: inline-block; 176 | } 177 | 178 | nav a.network { 179 | padding: 0 0.2em; 180 | } 181 | 182 | nav a.network:hover { 183 | text-decoration: none; 184 | } 185 | 186 | nav a.network .icon { 187 | fill: var(--light-primary); 188 | transition: fill 0.5s ease-in; 189 | } 190 | 191 | nav a.network:hover .icon { 192 | fill: var(--light-links); 193 | } 194 | 195 | /* Article Styles */ 196 | 197 | article { 198 | font-size: 1rem; 199 | max-width: 50ch; 200 | margin-bottom: 3rem; 201 | text-align: justify; 202 | scroll-margin-top: 5rem; 203 | line-height: 1.5rem; 204 | } 205 | 206 | article > h1 { 207 | font-size: 1.3rem; 208 | display: block; 209 | margin-bottom: 2rem; 210 | text-align: left; 211 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); 212 | } 213 | 214 | article h2 { 215 | font-size: 1.3rem; 216 | } 217 | 218 | article h3 { 219 | font-size: 1.1rem; 220 | } 221 | 222 | article a.permalink { 223 | display: block; 224 | pointer-events: all; 225 | margin: 2rem 0; 226 | } 227 | 228 | /* Webmentions Styles */ 229 | 230 | #webmentions { 231 | padding: 0.8rem 0.8rem 0 0.8rem; 232 | max-width: 50ch; 233 | } 234 | 235 | #webmentions h2 { 236 | font-size: 0.90rem; 237 | display: block; 238 | margin-top: 0; 239 | margin-bottom: 2rem; 240 | text-align: left; 241 | font-variant: small-caps; 242 | font-style: normal; 243 | } 244 | 245 | #webmentions ul { 246 | list-style: none; 247 | margin-left: 0; 248 | padding-left: 0; 249 | } 250 | 251 | #webmentions ul.comments li { 252 | display: block; 253 | min-height: 3em; 254 | margin-bottom: 1em; 255 | } 256 | 257 | #webmentions li a img { 258 | display: block; 259 | float: left; 260 | height: 3em; 261 | border-radius: 100%; 262 | margin-right: 1em; 263 | } 264 | 265 | #webmentions ul.reacts a { 266 | width: 3em; 267 | height: 3em; 268 | overflow: hidden; 269 | display: inline-block; 270 | margin-right: 1em; 271 | border-radius: 100%; 272 | } 273 | 274 | #webmentions ul.reacts a img { 275 | width: 100%; 276 | height: 100%; 277 | } 278 | 279 | /* Kudos Styles */ 280 | 281 | #kudos { 282 | text-align: center; 283 | position: relative; 284 | margin: 2em 0; 285 | } 286 | 287 | #kudos button { 288 | border-radius: 100%; 289 | margin: auto; 290 | padding: 0.5em 0.5em 0.3em 0.5em; 291 | background-color: var(--light-bg); 292 | border: 2px solid var(--light-primary); 293 | color: var(--light-bg); 294 | transition: all ease-in 0.25s; 295 | cursor: pointer; 296 | } 297 | 298 | #kudos button:hover, #kudos button.activated { 299 | background-color: var(--light-primary) !important; 300 | } 301 | 302 | #kudos button:hover .heart > path, #kudos button.activated .heart > path { 303 | stroke: var(--light-bg); 304 | } 305 | 306 | .heart > path { 307 | stroke: var(--light-primary); 308 | stroke-width: 2; 309 | fill: transparent; 310 | transition: fill 0.25s ease-in; 311 | } 312 | 313 | #kudos .counter { 314 | display: block; 315 | position: absolute; 316 | font-family: system-ui,sans-serif; 317 | top: -0.5rem; 318 | left: 52%; 319 | background-color: var(--light-primary); 320 | color: var(--light-bg); 321 | border: solid 1px var(--light-bg); 322 | font-weight: bold; 323 | padding: 0.5em; 324 | border-radius: 100%; 325 | line-height: 0.8rem; 326 | font-size: 0.8rem; 327 | } 328 | 329 | /* Dark Mode Styles */ 330 | 331 | @media (prefers-color-scheme: dark) { 332 | body { 333 | color: var(--dark-primary); 334 | background-color: var(--dark-bg); 335 | } 336 | 337 | a:hover { 338 | color: var(--dark-links); 339 | } 340 | 341 | nav a.network .icon { 342 | fill: var(--dark-links); 343 | } 344 | 345 | header h2, header h1 { 346 | color: var(--dark-primary); 347 | } 348 | 349 | nav a.network .icon { 350 | fill: var(--dark-primary); 351 | } 352 | 353 | article h1 { 354 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 355 | } 356 | 357 | #kudos button { 358 | background-color: var(--dark-bg); 359 | border: 2px solid var(--dark-primary); 360 | color: var(--dark-bg); 361 | } 362 | 363 | #kudos button:hover, #kudos button.activated { 364 | background-color: var(--dark-primary) !important; 365 | } 366 | 367 | 368 | #kudos button:hover .heart > path, #kudos button.activated .heart > path { 369 | stroke: var(--dark-bg); 370 | } 371 | 372 | .heart > path { 373 | stroke: var(--dark-primary); 374 | } 375 | 376 | #kudos .counter { 377 | background-color: var(--dark-primary); 378 | color: var(--dark-bg); 379 | border: solid 1px var(--dark-bg); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /assets/tools/feedwriter/Item.php: -------------------------------------------------------------------------------- 1 | 8 | * Copyright (C) 2010-2013 Michael Bemmerl 9 | * 10 | * This file is part of the "Universal Feed Writer" project. 11 | * 12 | * This program is free software: you can redistribute it and/or modify 13 | * it under the terms of the GNU General Public License as published by 14 | * the Free Software Foundation, either version 3 of the License, or 15 | * (at your option) any later version. 16 | * 17 | * This program is distributed in the hope that it will be useful, 18 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | * GNU General Public License for more details. 21 | * 22 | * You should have received a copy of the GNU General Public License 23 | * along with this program. If not, see . 24 | */ 25 | 26 | /** 27 | * Universal Feed Writer 28 | * 29 | * Item class - Used as feed element in Feed class 30 | * 31 | * @package UniversalFeedWriter 32 | * @author Anis uddin Ahmad 33 | * @link http://www.ajaxray.com/projects/rss 34 | */ 35 | class Item 36 | { 37 | /** 38 | * Collection of feed item elements 39 | */ 40 | private $elements = array(); 41 | 42 | /** 43 | * Contains the format of this feed. 44 | */ 45 | private $version; 46 | 47 | /** 48 | * Is used as a suffix when multiple elements have the same name. 49 | **/ 50 | private $_cpt = 0; 51 | 52 | /** 53 | * Constructor 54 | * 55 | * @param constant (RSS1/RSS2/ATOM) RSS2 is default. 56 | */ 57 | public function __construct($version = Feed::RSS2) 58 | { 59 | $this->version = $version; 60 | } 61 | 62 | /** 63 | * Return an unique number 64 | * 65 | * @access private 66 | * @return int 67 | **/ 68 | private function cpt() 69 | { 70 | return $this->_cpt++; 71 | } 72 | 73 | /** 74 | * Add an element to elements array 75 | * 76 | * @access public 77 | * @param string The tag name of an element 78 | * @param string The content of tag 79 | * @param array Attributes (if any) in 'attrName' => 'attrValue' format 80 | * @param boolean Specifies if an already existing element is overwritten. 81 | * @param boolean Specifies if multiple elements of the same name are allowed. 82 | * @return self 83 | */ 84 | public function addElement($elementName, $content, $attributes = null, $overwrite = FALSE, $allowMultiple = FALSE) 85 | { 86 | $key = $elementName; 87 | 88 | // return if element already exists & if overwriting is disabled 89 | // & if multiple elements are not allowed. 90 | if (isset($this->elements[$elementName]) && !$overwrite) { 91 | if (!$allowMultiple) 92 | return; 93 | 94 | $key .= '-' . $this->cpt(); 95 | } 96 | 97 | $this->elements[$key]['name'] = $elementName; 98 | $this->elements[$key]['content'] = $content; 99 | $this->elements[$key]['attributes'] = $attributes; 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Set multiple feed elements from an array. 106 | * Elements which have attributes cannot be added by this method 107 | * 108 | * @access public 109 | * @param array array of elements in 'tagName' => 'tagContent' format. 110 | * @return self 111 | */ 112 | public function addElementArray($elementArray) 113 | { 114 | if (!is_array($elementArray)) 115 | return; 116 | 117 | foreach ($elementArray as $elementName => $content) { 118 | $this->addElement($elementName, $content); 119 | } 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * Return the collection of elements in this feed item 126 | * 127 | * @access public 128 | * @return array All elements of this item. 129 | */ 130 | public function getElements() 131 | { 132 | return $this->elements; 133 | } 134 | 135 | /** 136 | * Return the type of this feed item 137 | * 138 | * @access public 139 | * @return string The feed type, as defined in Feed.php 140 | */ 141 | public function getVersion() 142 | { 143 | return $this->version; 144 | } 145 | 146 | // Wrapper functions ------------------------------------------------------ 147 | 148 | /** 149 | * Set the 'description' element of feed item 150 | * 151 | * @access public 152 | * @param string The content of 'description' or 'summary' element 153 | * @return self 154 | */ 155 | public function setDescription($description) 156 | { 157 | $tag = ($this->version == Feed::ATOM) ? 'summary' : 'description'; 158 | 159 | return $this->addElement($tag, $description); 160 | } 161 | 162 | /** 163 | * Set the 'content' element of the feed item 164 | * For ATOM feeds only 165 | * 166 | * @access public 167 | * @param string Content for the item (i.e., the body of a blog post). 168 | * @return self 169 | */ 170 | public function setContent($content) 171 | { 172 | if ($this->version != Feed::ATOM) 173 | die('The content element is supported in ATOM feeds only.'); 174 | 175 | return $this->addElement('content', $content, array('type' => 'html')); 176 | } 177 | 178 | /** 179 | * Set the 'title' element of feed item 180 | * 181 | * @access public 182 | * @param string The content of 'title' element 183 | * @return self 184 | */ 185 | public function setTitle($title) 186 | { 187 | return $this->addElement('title', $title); 188 | } 189 | 190 | /** 191 | * Set the 'date' element of the feed item. 192 | * 193 | * The value of the date parameter can be either an instance of the 194 | * DateTime class, an integer containing a UNIX timestamp or a string 195 | * which is parseable by PHP's 'strtotime' function. 196 | * 197 | * @access public 198 | * @param DateTime|int|string Date which should be used. 199 | * @return self 200 | */ 201 | public function setDate($date) 202 | { 203 | if (!is_numeric($date)) { 204 | if ($date instanceof DateTime) 205 | $date = $date->getTimestamp(); 206 | else { 207 | $date = strtotime($date); 208 | 209 | if ($date === FALSE) 210 | die('The given date string was not parseable.'); 211 | } 212 | } elseif ($date < 0) 213 | die('The given date is not an UNIX timestamp.'); 214 | 215 | if ($this->version == Feed::ATOM) { 216 | $tag = 'updated'; 217 | $value = date(\DATE_ATOM, $date); 218 | } elseif ($this->version == Feed::RSS2) { 219 | $tag = 'pubDate'; 220 | $value = date(\DATE_RSS, $date); 221 | } else { 222 | $tag = 'dc:date'; 223 | $value = date("Y-m-d", $date); 224 | } 225 | 226 | return $this->addElement($tag, $value); 227 | } 228 | 229 | /** 230 | * Set the 'link' element of feed item 231 | * 232 | * @access public 233 | * @param string The content of 'link' element 234 | * @return void 235 | */ 236 | public function setLink($link) 237 | { 238 | if ($this->version == Feed::RSS2 || $this->version == Feed::RSS1) { 239 | $this->addElement('link', $link); 240 | } else { 241 | $this->addElement('link','',array('href'=>$link)); 242 | $this->addElement('id', Feed::uuid($link,'urn:uuid:')); 243 | } 244 | 245 | return $this; 246 | } 247 | 248 | /** 249 | * Attach a external media to the feed item. 250 | * Not supported in RSS 1.0 feeds. 251 | * 252 | * See RFC 4288 for syntactical correct MIME types. 253 | * 254 | * Note that you should avoid the use of more than one enclosure in one item, 255 | * since some RSS aggregators don't support it. 256 | * 257 | * @access public 258 | * @param string The URL of the media. 259 | * @param integer The length of the media. 260 | * @param string The MIME type attribute of the media. 261 | * @param boolean Specifies, if multiple enclosures are allowed 262 | * @return self 263 | * @link https://tools.ietf.org/html/rfc4288 264 | */ 265 | public function addEnclosure($url, $length, $type, $multiple = TRUE) 266 | { 267 | if ($this->version == Feed::RSS1) 268 | die('Media attachment is not supported in RSS1 feeds.'); 269 | 270 | // the length parameter should be set to 0 if it can't be determined 271 | // see http://www.rssboard.org/rss-profile#element-channel-item-enclosure 272 | if (!is_numeric($length) || $length < 0) 273 | die('The length parameter must be an integer and greater or equals to zero.'); 274 | 275 | // Regex used from RFC 4287, page 41 276 | if (!is_string($type) || preg_match('/.+\/.+/', $type) != 1) 277 | die('type parameter must be a string and a MIME type.'); 278 | 279 | $attributes = array('length' => $length, 'type' => $type); 280 | 281 | if ($this->version == Feed::RSS2) { 282 | $attributes['url'] = $url; 283 | $this->addElement('enclosure', '', $attributes, FALSE, $multiple); 284 | } else { 285 | $attributes['href'] = $url; 286 | $attributes['rel'] = 'enclosure'; 287 | $this->addElement('atom:link', '', $attributes, FALSE, $multiple); 288 | } 289 | 290 | return $this; 291 | } 292 | 293 | /** 294 | * Alias of addEnclosure, for backward compatibility. Using only this 295 | * method ensures that the 'enclosure' element will be present only once. 296 | * 297 | * @access public 298 | * @param string The URL of the media. 299 | * @param integer The length of the media. 300 | * @param string The MIME type attribute of the media. 301 | * @return self 302 | * @link https://tools.ietf.org/html/rfc4288 303 | * @deprecated Use the addEnclosure method instead. 304 | * 305 | **/ 306 | public function setEnclosure($url, $length, $type) 307 | { 308 | return $this->addEnclosure($url, $length, $type, false); 309 | } 310 | 311 | /** 312 | * Set the 'author' element of feed item. 313 | * Not supported in RSS 1.0 feeds. 314 | * 315 | * @access public 316 | * @param string The author of this item 317 | * @param string Optional email address of the author 318 | * @param string Optional URI related to the author 319 | * @return self 320 | */ 321 | public function setAuthor($author, $email = null, $uri = null) 322 | { 323 | switch ($this->version) { 324 | case Feed::RSS1: die('The author element is not supported in RSS1 feeds.'); 325 | break; 326 | case Feed::RSS2: 327 | if ($email != null) 328 | $author = $email . ' (' . $author . ')'; 329 | 330 | $this->addElement('author', $author); 331 | break; 332 | case Feed::ATOM: 333 | $elements = array('name' => $author); 334 | 335 | // Regex from RFC 4287 page 41 336 | if ($email != null && preg_match('/.+@.+/', $email) == 1) 337 | $elements['email'] = $email; 338 | 339 | if ($uri != null) 340 | $elements['uri'] = $uri; 341 | 342 | $this->addElement('author', $elements); 343 | break; 344 | } 345 | 346 | return $this; 347 | } 348 | 349 | /** 350 | * Set the unique identifier of the feed item 351 | * 352 | * @access public 353 | * @param string The unique identifier of this item 354 | * @param boolean The value of the 'isPermaLink' attribute in RSS 2 feeds. 355 | * @return self 356 | */ 357 | public function setId($id, $permaLink = false) 358 | { 359 | if ($this->version == Feed::RSS2) { 360 | if (!is_bool($permaLink)) 361 | die('The permaLink parameter must be boolean.'); 362 | 363 | $permaLink = $permaLink ? 'true' : 'false'; 364 | 365 | $this->addElement('guid', $id, array('isPermaLink' => $permaLink)); 366 | } elseif ($this->version == Feed::ATOM) { 367 | $this->addElement('id', Feed::uuid($id,'urn:uuid:'), NULL, TRUE); 368 | } else 369 | die('A unique ID is not supported in RSS1 feeds.'); 370 | 371 | return $this; 372 | } 373 | 374 | } // end of class Item 375 | -------------------------------------------------------------------------------- /assets/tools/ParsedownExtra.php: -------------------------------------------------------------------------------- 1 | BlockTypes[':'] []= 'DefinitionList'; 32 | $this->BlockTypes['*'] []= 'Abbreviation'; 33 | 34 | # identify footnote definitions before reference definitions 35 | array_unshift($this->BlockTypes['['], 'Footnote'); 36 | 37 | # identify footnote markers before before links 38 | array_unshift($this->InlineTypes['['], 'FootnoteMarker'); 39 | } 40 | 41 | # 42 | # ~ 43 | 44 | function text($text) 45 | { 46 | $Elements = $this->textElements($text); 47 | 48 | # convert to markup 49 | $markup = $this->elements($Elements); 50 | 51 | # trim line breaks 52 | $markup = trim($markup, "\n"); 53 | 54 | # merge consecutive dl elements 55 | 56 | $markup = preg_replace('/<\/dl>\s+
\s+/', '', $markup); 57 | 58 | # add footnotes 59 | 60 | if (isset($this->DefinitionData['Footnote'])) 61 | { 62 | $Element = $this->buildFootnoteElement(); 63 | 64 | $markup .= "\n" . $this->element($Element); 65 | } 66 | 67 | return $markup; 68 | } 69 | 70 | # 71 | # Blocks 72 | # 73 | 74 | # 75 | # Abbreviation 76 | 77 | protected function blockAbbreviation($Line) 78 | { 79 | if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches)) 80 | { 81 | $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2]; 82 | 83 | $Block = array( 84 | 'hidden' => true, 85 | ); 86 | 87 | return $Block; 88 | } 89 | } 90 | 91 | # 92 | # Footnote 93 | 94 | protected function blockFootnote($Line) 95 | { 96 | if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches)) 97 | { 98 | $Block = array( 99 | 'label' => $matches[1], 100 | 'text' => $matches[2], 101 | 'hidden' => true, 102 | ); 103 | 104 | return $Block; 105 | } 106 | } 107 | 108 | protected function blockFootnoteContinue($Line, $Block) 109 | { 110 | if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text'])) 111 | { 112 | return; 113 | } 114 | 115 | if (isset($Block['interrupted'])) 116 | { 117 | if ($Line['indent'] >= 4) 118 | { 119 | $Block['text'] .= "\n\n" . $Line['text']; 120 | 121 | return $Block; 122 | } 123 | } 124 | else 125 | { 126 | $Block['text'] .= "\n" . $Line['text']; 127 | 128 | return $Block; 129 | } 130 | } 131 | 132 | protected function blockFootnoteComplete($Block) 133 | { 134 | $this->DefinitionData['Footnote'][$Block['label']] = array( 135 | 'text' => $Block['text'], 136 | 'count' => null, 137 | 'number' => null, 138 | ); 139 | 140 | return $Block; 141 | } 142 | 143 | # 144 | # Definition List 145 | 146 | protected function blockDefinitionList($Line, $Block) 147 | { 148 | if ( ! isset($Block) or $Block['type'] !== 'Paragraph') 149 | { 150 | return; 151 | } 152 | 153 | $Element = array( 154 | 'name' => 'dl', 155 | 'elements' => array(), 156 | ); 157 | 158 | $terms = explode("\n", $Block['element']['handler']['argument']); 159 | 160 | foreach ($terms as $term) 161 | { 162 | $Element['elements'] []= array( 163 | 'name' => 'dt', 164 | 'handler' => array( 165 | 'function' => 'lineElements', 166 | 'argument' => $term, 167 | 'destination' => 'elements' 168 | ), 169 | ); 170 | } 171 | 172 | $Block['element'] = $Element; 173 | 174 | $Block = $this->addDdElement($Line, $Block); 175 | 176 | return $Block; 177 | } 178 | 179 | protected function blockDefinitionListContinue($Line, array $Block) 180 | { 181 | if ($Line['text'][0] === ':') 182 | { 183 | $Block = $this->addDdElement($Line, $Block); 184 | 185 | return $Block; 186 | } 187 | else 188 | { 189 | if (isset($Block['interrupted']) and $Line['indent'] === 0) 190 | { 191 | return; 192 | } 193 | 194 | if (isset($Block['interrupted'])) 195 | { 196 | $Block['dd']['handler']['function'] = 'textElements'; 197 | $Block['dd']['handler']['argument'] .= "\n\n"; 198 | 199 | $Block['dd']['handler']['destination'] = 'elements'; 200 | 201 | unset($Block['interrupted']); 202 | } 203 | 204 | $text = substr($Line['body'], min($Line['indent'], 4)); 205 | 206 | $Block['dd']['handler']['argument'] .= "\n" . $text; 207 | 208 | return $Block; 209 | } 210 | } 211 | 212 | # 213 | # Header 214 | 215 | protected function blockHeader($Line) 216 | { 217 | $Block = parent::blockHeader($Line); 218 | 219 | if ($Block !== null && preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE)) 220 | { 221 | $attributeString = $matches[1][0]; 222 | 223 | $Block['element']['attributes'] = $this->parseAttributeData($attributeString); 224 | 225 | $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]); 226 | } 227 | 228 | return $Block; 229 | } 230 | 231 | # 232 | # Markup 233 | 234 | protected function blockMarkup($Line) 235 | { 236 | if ($this->markupEscaped or $this->safeMode) 237 | { 238 | return; 239 | } 240 | 241 | if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) 242 | { 243 | $element = strtolower($matches[1]); 244 | 245 | if (in_array($element, $this->textLevelElements)) 246 | { 247 | return; 248 | } 249 | 250 | $Block = array( 251 | 'name' => $matches[1], 252 | 'depth' => 0, 253 | 'element' => array( 254 | 'rawHtml' => $Line['text'], 255 | 'autobreak' => true, 256 | ), 257 | ); 258 | 259 | $length = strlen($matches[0]); 260 | $remainder = substr($Line['text'], $length); 261 | 262 | if (trim($remainder) === '') 263 | { 264 | if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) 265 | { 266 | $Block['closed'] = true; 267 | $Block['void'] = true; 268 | } 269 | } 270 | else 271 | { 272 | if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) 273 | { 274 | return; 275 | } 276 | if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) 277 | { 278 | $Block['closed'] = true; 279 | } 280 | } 281 | 282 | return $Block; 283 | } 284 | } 285 | 286 | protected function blockMarkupContinue($Line, array $Block) 287 | { 288 | if (isset($Block['closed'])) 289 | { 290 | return; 291 | } 292 | 293 | if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open 294 | { 295 | $Block['depth'] ++; 296 | } 297 | 298 | if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close 299 | { 300 | if ($Block['depth'] > 0) 301 | { 302 | $Block['depth'] --; 303 | } 304 | else 305 | { 306 | $Block['closed'] = true; 307 | } 308 | } 309 | 310 | if (isset($Block['interrupted'])) 311 | { 312 | $Block['element']['rawHtml'] .= "\n"; 313 | unset($Block['interrupted']); 314 | } 315 | 316 | $Block['element']['rawHtml'] .= "\n".$Line['body']; 317 | 318 | return $Block; 319 | } 320 | 321 | protected function blockMarkupComplete($Block) 322 | { 323 | if ( ! isset($Block['void'])) 324 | { 325 | $Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']); 326 | } 327 | 328 | return $Block; 329 | } 330 | 331 | # 332 | # Setext 333 | 334 | protected function blockSetextHeader($Line, ?array $Block = null) 335 | { 336 | $Block = parent::blockSetextHeader($Line, $Block); 337 | 338 | if ($Block !== null && preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE)) 339 | { 340 | $attributeString = $matches[1][0]; 341 | 342 | $Block['element']['attributes'] = $this->parseAttributeData($attributeString); 343 | 344 | $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]); 345 | } 346 | 347 | return $Block; 348 | } 349 | 350 | # 351 | # Inline Elements 352 | # 353 | 354 | # 355 | # Footnote Marker 356 | 357 | protected function inlineFootnoteMarker($Excerpt) 358 | { 359 | if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) 360 | { 361 | $name = $matches[1]; 362 | 363 | if ( ! isset($this->DefinitionData['Footnote'][$name])) 364 | { 365 | return; 366 | } 367 | 368 | $this->DefinitionData['Footnote'][$name]['count'] ++; 369 | 370 | if ( ! isset($this->DefinitionData['Footnote'][$name]['number'])) 371 | { 372 | $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » & 373 | } 374 | 375 | $Element = array( 376 | 'name' => 'sup', 377 | 'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name), 378 | 'element' => array( 379 | 'name' => 'a', 380 | 'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'), 381 | 'text' => $this->DefinitionData['Footnote'][$name]['number'], 382 | ), 383 | ); 384 | 385 | return array( 386 | 'extent' => strlen($matches[0]), 387 | 'element' => $Element, 388 | ); 389 | } 390 | } 391 | 392 | private $footnoteCount = 0; 393 | 394 | # 395 | # Link 396 | 397 | protected function inlineLink($Excerpt) 398 | { 399 | $Link = parent::inlineLink($Excerpt); 400 | 401 | $remainder = $Link !== null ? substr($Excerpt['text'], $Link['extent']) : ''; 402 | 403 | if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches)) 404 | { 405 | $Link['element']['attributes'] += $this->parseAttributeData($matches[1]); 406 | 407 | $Link['extent'] += strlen($matches[0]); 408 | } 409 | 410 | return $Link; 411 | } 412 | 413 | # 414 | # ~ 415 | # 416 | 417 | private $currentAbreviation; 418 | private $currentMeaning; 419 | 420 | protected function insertAbreviation(array $Element) 421 | { 422 | if (isset($Element['text'])) 423 | { 424 | $Element['elements'] = self::pregReplaceElements( 425 | '/\b'.preg_quote($this->currentAbreviation, '/').'\b/', 426 | array( 427 | array( 428 | 'name' => 'abbr', 429 | 'attributes' => array( 430 | 'title' => $this->currentMeaning, 431 | ), 432 | 'text' => $this->currentAbreviation, 433 | ) 434 | ), 435 | $Element['text'] 436 | ); 437 | 438 | unset($Element['text']); 439 | } 440 | 441 | return $Element; 442 | } 443 | 444 | protected function inlineText($text) 445 | { 446 | $Inline = parent::inlineText($text); 447 | 448 | if (isset($this->DefinitionData['Abbreviation'])) 449 | { 450 | foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning) 451 | { 452 | $this->currentAbreviation = $abbreviation; 453 | $this->currentMeaning = $meaning; 454 | 455 | $Inline['element'] = $this->elementApplyRecursiveDepthFirst( 456 | array($this, 'insertAbreviation'), 457 | $Inline['element'] 458 | ); 459 | } 460 | } 461 | 462 | return $Inline; 463 | } 464 | 465 | # 466 | # Util Methods 467 | # 468 | 469 | protected function addDdElement(array $Line, array $Block) 470 | { 471 | $text = substr($Line['text'], 1); 472 | $text = trim($text); 473 | 474 | unset($Block['dd']); 475 | 476 | $Block['dd'] = array( 477 | 'name' => 'dd', 478 | 'handler' => array( 479 | 'function' => 'lineElements', 480 | 'argument' => $text, 481 | 'destination' => 'elements' 482 | ), 483 | ); 484 | 485 | if (isset($Block['interrupted'])) 486 | { 487 | $Block['dd']['handler']['function'] = 'textElements'; 488 | 489 | unset($Block['interrupted']); 490 | } 491 | 492 | $Block['element']['elements'] []= & $Block['dd']; 493 | 494 | return $Block; 495 | } 496 | 497 | protected function buildFootnoteElement() 498 | { 499 | $Element = array( 500 | 'name' => 'div', 501 | 'attributes' => array('class' => 'footnotes'), 502 | 'elements' => array( 503 | array('name' => 'hr'), 504 | array( 505 | 'name' => 'ol', 506 | 'elements' => array(), 507 | ), 508 | ), 509 | ); 510 | 511 | uasort($this->DefinitionData['Footnote'], [self::class, 'sortFootnotes']); 512 | 513 | foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData) 514 | { 515 | if ( ! isset($DefinitionData['number'])) 516 | { 517 | continue; 518 | } 519 | 520 | $text = $DefinitionData['text']; 521 | 522 | $textElements = parent::textElements($text); 523 | 524 | $numbers = range(1, $DefinitionData['count']); 525 | 526 | $backLinkElements = array(); 527 | 528 | foreach ($numbers as $number) 529 | { 530 | $backLinkElements[] = array('text' => ' '); 531 | $backLinkElements[] = array( 532 | 'name' => 'a', 533 | 'attributes' => array( 534 | 'href' => "#fnref$number:$definitionId", 535 | 'rev' => 'footnote', 536 | 'class' => 'footnote-backref', 537 | ), 538 | 'rawHtml' => '↩', 539 | 'allowRawHtmlInSafeMode' => true, 540 | 'autobreak' => false, 541 | ); 542 | } 543 | 544 | unset($backLinkElements[0]); 545 | 546 | $n = count($textElements) -1; 547 | 548 | if ($textElements[$n]['name'] === 'p') 549 | { 550 | $backLinkElements = array_merge( 551 | array( 552 | array( 553 | 'rawHtml' => ' ', 554 | 'allowRawHtmlInSafeMode' => true, 555 | ), 556 | ), 557 | $backLinkElements 558 | ); 559 | 560 | unset($textElements[$n]['name']); 561 | 562 | $textElements[$n] = array( 563 | 'name' => 'p', 564 | 'elements' => array_merge( 565 | array($textElements[$n]), 566 | $backLinkElements 567 | ), 568 | ); 569 | } 570 | else 571 | { 572 | $textElements[] = array( 573 | 'name' => 'p', 574 | 'elements' => $backLinkElements 575 | ); 576 | } 577 | 578 | $Element['elements'][1]['elements'] []= array( 579 | 'name' => 'li', 580 | 'attributes' => array('id' => 'fn:'.$definitionId), 581 | 'elements' => array_merge( 582 | $textElements 583 | ), 584 | ); 585 | } 586 | 587 | return $Element; 588 | } 589 | 590 | # ~ 591 | 592 | protected function parseAttributeData($attributeString) 593 | { 594 | $Data = array(); 595 | 596 | $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY); 597 | 598 | foreach ($attributes as $attribute) 599 | { 600 | if ($attribute[0] === '#') 601 | { 602 | $Data['id'] = substr($attribute, 1); 603 | } 604 | else # "." 605 | { 606 | $classes []= substr($attribute, 1); 607 | } 608 | } 609 | 610 | if (isset($classes)) 611 | { 612 | $Data['class'] = implode(' ', $classes); 613 | } 614 | 615 | return $Data; 616 | } 617 | 618 | # ~ 619 | 620 | protected function processTag($elementMarkup) # recursive 621 | { 622 | # http://stackoverflow.com/q/1148928/200145 623 | libxml_use_internal_errors(true); 624 | 625 | $DOMDocument = new DOMDocument; 626 | 627 | # http://stackoverflow.com/q/11309194/200145 628 | $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8'); 629 | 630 | # http://stackoverflow.com/q/4879946/200145 631 | $DOMDocument->loadHTML($elementMarkup); 632 | $DOMDocument->removeChild($DOMDocument->doctype); 633 | $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild); 634 | 635 | $elementText = ''; 636 | 637 | if ($DOMDocument->documentElement->getAttribute('markdown') === '1') 638 | { 639 | foreach ($DOMDocument->documentElement->childNodes as $Node) 640 | { 641 | $elementText .= $DOMDocument->saveHTML($Node); 642 | } 643 | 644 | $DOMDocument->documentElement->removeAttribute('markdown'); 645 | 646 | $elementText = "\n".$this->text($elementText)."\n"; 647 | } 648 | else 649 | { 650 | foreach ($DOMDocument->documentElement->childNodes as $Node) 651 | { 652 | $nodeMarkup = $DOMDocument->saveHTML($Node); 653 | 654 | if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements)) 655 | { 656 | $elementText .= $this->processTag($nodeMarkup); 657 | } 658 | else 659 | { 660 | $elementText .= $nodeMarkup; 661 | } 662 | } 663 | } 664 | 665 | # because we don't want for markup to get encoded 666 | $DOMDocument->documentElement->nodeValue = 'placeholder\x1A'; 667 | 668 | $markup = $DOMDocument->saveHTML($DOMDocument->documentElement); 669 | $markup = str_replace('placeholder\x1A', $elementText, $markup); 670 | 671 | return $markup; 672 | } 673 | 674 | # ~ 675 | 676 | protected function sortFootnotes($A, $B) # callback 677 | { 678 | return $A['number'] - $B['number']; 679 | } 680 | 681 | # 682 | # Fields 683 | # 684 | 685 | protected $regexAttribute = '(?:[#.][-\w]+[ ]*)'; 686 | } 687 | -------------------------------------------------------------------------------- /assets/tools/feedwriter/Feed.php: -------------------------------------------------------------------------------- 1 | 8 | * Copyright (C) 2010-2014 Michael Bemmerl 9 | * 10 | * This file is part of the "Universal Feed Writer" project. 11 | * 12 | * This program is free software: you can redistribute it and/or modify 13 | * it under the terms of the GNU General Public License as published by 14 | * the Free Software Foundation, either version 3 of the License, or 15 | * (at your option) any later version. 16 | * 17 | * This program is distributed in the hope that it will be useful, 18 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | * GNU General Public License for more details. 21 | * 22 | * You should have received a copy of the GNU General Public License 23 | * along with this program. If not, see . 24 | */ 25 | 26 | // RSS 0.90 Officially obsoleted by 1.0 27 | // RSS 0.91, 0.92, 0.93 and 0.94 Officially obsoleted by 2.0 28 | // So, define constants for RSS 1.0, RSS 2.0 and ATOM 29 | 30 | /** 31 | * Universal Feed Writer class 32 | * 33 | * Generate RSS 1.0, RSS2.0 and ATOM Feeds 34 | * 35 | * @package UniversalFeedWriter 36 | * @author Anis uddin Ahmad 37 | * @link http://www.ajaxray.com/projects/rss 38 | */ 39 | abstract class Feed 40 | { 41 | const RSS1 = 'RSS 1.0'; 42 | const RSS2 = 'RSS 2.0'; 43 | const ATOM = 'ATOM'; 44 | 45 | /** 46 | * Collection of all channel elements 47 | */ 48 | private $encoding; 49 | private $channels = array(); 50 | 51 | /** 52 | * Collection of items as object of \FeedWriter\Item class. 53 | */ 54 | private $items = array(); 55 | 56 | /** 57 | * Store some other version wise data 58 | */ 59 | private $data = array(); 60 | 61 | /** 62 | * The tag names which have to encoded as CDATA 63 | */ 64 | private $CDATAEncoding = array(); 65 | 66 | /** 67 | * Collection of XML namespaces 68 | */ 69 | private $namespaces = array(); 70 | 71 | /** 72 | * Contains the format of this feed. 73 | */ 74 | private $version = null; 75 | 76 | /** 77 | * Constructor 78 | * 79 | * If no version is given, a feed in RSS 2.0 format will be generated. 80 | * 81 | * @param constant the version constant (RSS1/RSS2/ATOM). 82 | */ 83 | protected function __construct($version = Feed::RSS2) 84 | { 85 | $this->version = $version; 86 | 87 | // Setting default encoding 88 | $this->encoding = 'utf-8'; 89 | 90 | // Setting default value for essential channel elements 91 | $this->channels['title'] = $version . ' Feed'; 92 | $this->channels['link'] = 'http://www.ajaxray.com/blog'; 93 | 94 | // Add some default XML namespaces 95 | $this->namespaces['content'] = 'http://purl.org/rss/1.0/modules/content/'; 96 | $this->namespaces['wfw'] = 'http://wellformedweb.org/CommentAPI/'; 97 | $this->namespaces['atom'] = 'http://www.w3.org/2005/Atom'; 98 | $this->namespaces['rdf'] = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; 99 | $this->namespaces['rss1'] = 'http://purl.org/rss/1.0/'; 100 | $this->namespaces['dc'] = 'http://purl.org/dc/elements/1.1/'; 101 | $this->namespaces['sy'] = 'http://purl.org/rss/1.0/modules/syndication/'; 102 | 103 | // Tag names to encode in CDATA 104 | $this->addCDATAEncoding(array('description', 'content:encoded', 'summary')); 105 | } 106 | 107 | // Start # public functions --------------------------------------------- 108 | 109 | /** 110 | * Set the URLs for feed pagination. 111 | * 112 | * See RFC 5005, chapter 3. At least one page URL must be specified. 113 | * 114 | * @param string The URL to the next page of this feed. Optional. 115 | * @param string The URL to the previous page of this feed. Optional. 116 | * @param string The URL to the first page of this feed. Optional. 117 | * @param string The URL to the last page of this feed. Optional. 118 | * @link http://tools.ietf.org/html/rfc5005#section-3 119 | * @return self 120 | */ 121 | public function setPagination($nextURL = null, $previousURL = null, $firstURL = null, $lastURL = null) 122 | { 123 | if (empty($nextURL) && empty($previousURL) && empty($firstURL) && empty($lastURL)) 124 | die('At least one URL must be specified for pagination to work.'); 125 | 126 | if (!empty($nextURL)) 127 | $this->setAtomLink($nextURL, 'next'); 128 | 129 | if (!empty($previousURL)) 130 | $this->setAtomLink($previousURL, 'previous'); 131 | 132 | if (!empty($firstURL)) 133 | $this->setAtomLink($firstURL, 'first'); 134 | 135 | if (!empty($lastURL)) 136 | $this->setAtomLink($lastURL, 'last'); 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * Add a channel element indicating the program used to generate the feed. 143 | * 144 | * @return self 145 | */ 146 | public function addGenerator() 147 | { 148 | if ($this->version == Feed::ATOM) 149 | $this->setChannelElement('atom:generator', 'FeedWriter', array('uri' => 'https://github.com/mibe/FeedWriter')); 150 | else if ($this->version == Feed::RSS2) 151 | $this->setChannelElement('generator', 'FeedWriter'); 152 | else 153 | die('The generator element is not supported in RSS1 feeds.'); 154 | 155 | return $this; 156 | } 157 | 158 | /** 159 | * Add a XML namespace to the internal list of namespaces. After that, 160 | * custom channel elements can be used properly to generate a valid feed. 161 | * 162 | * @access public 163 | * @param string namespace prefix 164 | * @param string namespace name (URI) 165 | * @return self 166 | * @link http://www.w3.org/TR/REC-xml-names/ 167 | */ 168 | public function addNamespace($prefix, $uri) 169 | { 170 | $this->namespaces[$prefix] = $uri; 171 | 172 | return $this; 173 | } 174 | 175 | /** 176 | * Add a channel element to the feed. 177 | * 178 | * @access public 179 | * @param string name of the channel tag 180 | * @param string content of the channel tag 181 | * @param array array of element attributes with attribute name as array key 182 | * @param bool TRUE if this element can appear multiple times 183 | * @return self 184 | */ 185 | public function setChannelElement($elementName, $content, $attributes = null, $multiple = false) 186 | { 187 | $entity['content'] = $content; 188 | $entity['attributes'] = $attributes; 189 | 190 | if ($multiple === TRUE) 191 | $this->channels[$elementName][] = $entity; 192 | else 193 | $this->channels[$elementName] = $entity; 194 | 195 | return $this; 196 | } 197 | 198 | /** 199 | * Set multiple channel elements from an array. Array elements 200 | * should be 'channelName' => 'channelContent' format. 201 | * 202 | * @access public 203 | * @param array array of channels 204 | * @return self 205 | */ 206 | public function setChannelElementsFromArray($elementArray) 207 | { 208 | if (!is_array($elementArray)) 209 | return; 210 | 211 | foreach ($elementArray as $elementName => $content) { 212 | $this->setChannelElement($elementName, $content); 213 | } 214 | 215 | return $this; 216 | } 217 | 218 | /** 219 | * Get the appropriate MIME type string for the current feed. 220 | * 221 | * @access public 222 | * @return string The MIME type string. 223 | */ 224 | public function getMIMEType() 225 | { 226 | switch ($this->version) { 227 | case Feed::RSS2 : $mimeType = "application/rss+xml"; 228 | break; 229 | case Feed::RSS1 : $mimeType = "application/rdf+xml"; 230 | break; 231 | case Feed::ATOM : $mimeType = "application/atom+xml"; 232 | break; 233 | default : $mimeType = "text/xml"; 234 | } 235 | 236 | return $mimeType; 237 | } 238 | 239 | /** 240 | * Print the actual RSS/ATOM file 241 | * 242 | * Sets a Content-Type header and echoes the contents of the feed. 243 | * Should only be used in situations where direct output is desired; 244 | * if you need to pass a string around, use generateFeed() instead. 245 | * 246 | * @access public 247 | * @param bool FALSE if the specific feed media type should be sent. 248 | * @return void 249 | */ 250 | public function printFeed($useGenericContentType = false) 251 | { 252 | $contentType = "text/xml"; 253 | 254 | if (!$useGenericContentType) { 255 | $contentType = $this->getMIMEType(); 256 | } 257 | 258 | header("Content-Type: " . $contentType); 259 | echo $this->generateFeed(); 260 | } 261 | 262 | /** 263 | * Generate the feed. 264 | * 265 | * @access public 266 | * @return string The complete feed XML. 267 | */ 268 | public function generateFeed() 269 | { 270 | return $this->makeHeader() 271 | . $this->makeChannels() 272 | . $this->makeItems() 273 | . $this->makeFooter(); 274 | } 275 | 276 | /** 277 | * Create a new Item. 278 | * 279 | * @access public 280 | * @return Item instance of Item class 281 | */ 282 | public function createNewItem() 283 | { 284 | $Item = new Item($this->version); 285 | 286 | return $Item; 287 | } 288 | 289 | /** 290 | * Add a properties to be CDATA encoded 291 | * 292 | * @access public 293 | * @param array An array of properties that are merged into the list of properties should be encoded as CDATA 294 | * @return self 295 | */ 296 | public function addCDATAEncoding(Array $property_list) 297 | { 298 | $this->CDATAEncoding = array_merge($this->CDATAEncoding, $property_list); 299 | 300 | return $this; 301 | } 302 | 303 | /** 304 | * Get list of CDATA encoded properties 305 | * 306 | * @access public 307 | * @return array Return an array of CDATA properties that are to be encoded as CDATA 308 | */ 309 | public function getCDATAEncoding() 310 | { 311 | return $this->CDATAEncoding; 312 | } 313 | 314 | /** 315 | * Add a FeedItem to the main class 316 | * 317 | * @access public 318 | * @param Item instance of Item class 319 | * @return self 320 | */ 321 | public function addItem(Item $feedItem) 322 | { 323 | if ($feedItem->getVersion() != $this->version) 324 | die('Feed type mismatch: This instance can handle ' . $this->version . ' feeds only, but item with type ' . $feedItem->getVersion() . ' given.'); 325 | 326 | $this->items[] = $feedItem; 327 | 328 | return $this; 329 | } 330 | 331 | // Wrapper functions ------------------------------------------------------------------- 332 | 333 | /** 334 | * Set the 'encoding' attribute in the XML prolog. 335 | * 336 | * @access public 337 | * @param string value of 'encoding' attribute 338 | * @return self 339 | */ 340 | public function setEncoding($encoding) 341 | { 342 | $this->encoding = $encoding; 343 | 344 | return $this; 345 | } 346 | 347 | /** 348 | * Set the 'title' channel element 349 | * 350 | * @access public 351 | * @param string value of 'title' channel tag 352 | * @return self 353 | */ 354 | public function setTitle($title) 355 | { 356 | return $this->setChannelElement('title', $title); 357 | } 358 | 359 | /** 360 | * Set the date when the ATOM feed was lastly updated. 361 | * 362 | * This adds the 'updated' element to the feed. The value of the date parameter 363 | * can be either an instance of the DateTime class, an integer containing a UNIX 364 | * timestamp or a string which is parseable by PHP's 'strtotime' function. 365 | * 366 | * Not supported in RSS1 feeds. 367 | * 368 | * @access public 369 | * @param DateTime|int|string Date which should be used. 370 | * @return self 371 | */ 372 | public function setDate($date) 373 | { 374 | if ($this->version == Feed::RSS1) 375 | die('The publication date is not supported in RSS1 feeds.'); 376 | 377 | // The feeds have different date formats. 378 | $format = $this->version == Feed::ATOM ? \DATE_ATOM : \DATE_RSS; 379 | 380 | if ($date instanceof DateTime) 381 | $date = $date->format($format); 382 | else if(is_numeric($date) && $date >= 0) 383 | $date = date($format, $date); 384 | else if (is_string($date)) 385 | $date = date($format, strtotime($date)); 386 | else 387 | die('The given date was not an instance of DateTime, a UNIX timestamp or a date string.'); 388 | 389 | if ($this->version == Feed::ATOM) 390 | $this->setChannelElement('updated', $date); 391 | else 392 | $this->setChannelElement('lastBuildDate', $date); 393 | 394 | return $this; 395 | } 396 | 397 | /** 398 | * Set the 'description' channel element 399 | * 400 | * @access public 401 | * @param string value of 'description' channel tag 402 | * @return self 403 | */ 404 | public function setDescription($description) 405 | { 406 | if ($this->version != Feed::ATOM) 407 | $this->setChannelElement('description', $description); 408 | 409 | return $this; 410 | } 411 | 412 | /** 413 | * Set the 'link' channel element 414 | * 415 | * @access public 416 | * @param string value of 'link' channel tag 417 | * @return self 418 | */ 419 | public function setLink($link) 420 | { 421 | if ($this->version == Feed::ATOM) 422 | $this->setChannelElement('link', '', array('href' => $link)); 423 | else 424 | $this->setChannelElement('link', $link); 425 | 426 | return $this; 427 | } 428 | 429 | /** 430 | * Set custom 'link' channel elements. 431 | * 432 | * In ATOM feeds, only one link with alternate relation and the same combination of 433 | * type and hreflang values. 434 | * 435 | * @access public 436 | * @param string URI of this link 437 | * @param string relation type of the resource 438 | * @param string MIME type of the target resource 439 | * @param string language of the resource 440 | * @param string human-readable information about the resource 441 | * @param int length of the resource in bytes 442 | * @link https://www.iana.org/assignments/link-relations/link-relations.xml 443 | * @link https://tools.ietf.org/html/rfc4287#section-4.2.7 444 | * @return self 445 | */ 446 | public function setAtomLink($href, $rel = null, $type = null, $hreflang = null, $title = null, $length = null) 447 | { 448 | $data = array('href' => $href); 449 | 450 | if ($rel != null) { 451 | if (!is_string($rel) || empty($rel)) 452 | die('rel parameter must be a string and a valid relation identifier.'); 453 | 454 | $data['rel'] = $rel; 455 | } 456 | if ($type != null) { 457 | // Regex used from RFC 4287, page 41 458 | if (!is_string($type) || preg_match('/.+\/.+/', $type) != 1) 459 | die('type parameter must be a string and a MIME type.'); 460 | 461 | $data['type'] = $type; 462 | } 463 | if ($hreflang != null) { 464 | // Regex used from RFC 4287, page 41 465 | if (!is_string($hreflang) || preg_match('/[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*/', $hreflang) != 1) 466 | die('hreflang parameter must be a string and a valid language code.'); 467 | 468 | $data['hreflang'] = $hreflang; 469 | } 470 | if ($title != null) { 471 | if (!is_string($title) || empty($title)) 472 | die('title parameter must be a string and not empty.'); 473 | 474 | $data['title'] = $title; 475 | } 476 | if ($length != null) { 477 | if (!is_int($length) || $length < 0) 478 | die('length parameter must be a positive integer.'); 479 | 480 | $data['length'] = (string) $length; 481 | } 482 | 483 | // ATOM spec. has some restrictions on atom:link usage 484 | // See RFC 4287, page 12 (4.1.1) 485 | if ($this->version == Feed::ATOM) { 486 | foreach ($this->channels as $key => $value) { 487 | if ($key != 'atom:link') 488 | continue; 489 | 490 | // $value is an array , so check every element 491 | foreach ($value as $linkItem) { 492 | // Only one link with relation alternate and same hreflang & type is allowed. 493 | if (@$linkItem['rel'] == 'alternate' && @$linkItem['hreflang'] == $hreflang && @$linkItem['type'] == $type) 494 | die('The feed must not contain more than one link element with a relation of "alternate"' 495 | . ' that has the same combination of type and hreflang attribute values.'); 496 | } 497 | } 498 | } 499 | 500 | return $this->setChannelElement('atom:link', '', $data, true); 501 | } 502 | 503 | /** 504 | * Set an 'atom:link' channel element with relation=self attribute. 505 | * Needs the full URL to this feed. 506 | * 507 | * @link http://www.rssboard.org/rss-profile#namespace-elements-atom-link 508 | * @access public 509 | * @param string URL to this feed 510 | * @return self 511 | */ 512 | public function setSelfLink($url) 513 | { 514 | return $this->setAtomLink($url, 'self', $this->getMIMEType()); 515 | } 516 | 517 | /** 518 | * Set the 'image' channel element 519 | * 520 | * @access public 521 | * @param string title of image 522 | * @param string link url of the image 523 | * @param string path url of the image 524 | * @return self 525 | */ 526 | public function setImage($title, $link, $url) 527 | { 528 | return $this->setChannelElement('image', array('title'=>$title, 'link'=>$link, 'url'=>$url)); 529 | } 530 | 531 | /** 532 | * Set the 'about' channel element. Only for RSS 1.0 533 | * 534 | * @access public 535 | * @param string value of 'about' channel tag 536 | * @return self 537 | */ 538 | public function setChannelAbout($url) 539 | { 540 | $this->data['ChannelAbout'] = $url; 541 | 542 | return $this; 543 | } 544 | 545 | /** 546 | * Generate an UUID. 547 | * 548 | * The UUID is based on an MD5 hash. If no key is given, a unique ID as the input 549 | * for the MD5 hash is generated. 550 | * 551 | * @author Anis uddin Ahmad 552 | * @param string optional key on which the UUID is generated 553 | * @param string an optional prefix 554 | * @return string the formated UUID 555 | */ 556 | public static function uuid($key = null, $prefix = '') 557 | { 558 | $key = ($key == null) ? uniqid(rand()) : $key; 559 | $chars = md5($key); 560 | $uuid = substr($chars,0,8) . '-'; 561 | $uuid .= substr($chars,8,4) . '-'; 562 | $uuid .= substr($chars,12,4) . '-'; 563 | $uuid .= substr($chars,16,4) . '-'; 564 | $uuid .= substr($chars,20,12); 565 | 566 | return $prefix . $uuid; 567 | } 568 | 569 | /** 570 | * Replace invalid xml utf-8 chars. 571 | * 572 | * See utf8_for_xml() function at 573 | * http://www.phpwact.org/php/i18n/charsets#xml and 574 | * http://www.w3.org/TR/REC-xml/#charsets 575 | * 576 | * @param string 577 | * @return string 578 | */ 579 | public static function utf8_for_xml($string) 580 | { 581 | return preg_replace('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u', ' ', $string); 582 | } 583 | // End # public functions ---------------------------------------------- 584 | 585 | // Start # private functions ---------------------------------------------- 586 | 587 | /** 588 | * Returns all used XML namespace prefixes in this instance. 589 | * This includes all channel elements and feed items. 590 | * Unfortunately some namespace prefixes are not included, 591 | * because they are hardcoded, e.g. rdf. 592 | * 593 | * @access private 594 | * @return array Array with namespace prefix as value. 595 | */ 596 | private function getNamespacePrefixes() 597 | { 598 | $prefixes = array(); 599 | 600 | // Get all tag names from channel elements... 601 | $tags = array_keys($this->channels); 602 | 603 | // ... and now all names from feed items 604 | foreach ($this->items as $item) 605 | $tags = array_merge($tags, array_keys($item->getElements())); 606 | 607 | // Look for prefixes in those tag names 608 | foreach ($tags as $tag) { 609 | $elements = explode(':', $tag); 610 | 611 | if (count($elements) != 2) 612 | continue; 613 | 614 | $prefixes[] = $elements[0]; 615 | } 616 | 617 | return array_unique($prefixes); 618 | } 619 | 620 | /** 621 | * Returns the XML header and root element, depending on the feed type. 622 | * 623 | * @access private 624 | * @return string The XML header of the feed. 625 | */ 626 | private function makeHeader() 627 | { 628 | $out = 'encoding.'" ?>' . PHP_EOL; 629 | 630 | $prefixes = $this->getNamespacePrefixes(); 631 | $attributes = array(); 632 | $tagName = ''; 633 | $defaultNamespace = ''; 634 | 635 | if ($this->version == Feed::RSS2) { 636 | $tagName = 'rss'; 637 | $attributes['version'] = '2.0'; 638 | } elseif ($this->version == Feed::RSS1) { 639 | $tagName = 'rdf:RDF'; 640 | $prefixes[] = 'rdf'; 641 | $defaultNamespace = $this->namespaces['rss1']; 642 | } elseif ($this->version == Feed::ATOM) { 643 | $tagName = 'feed'; 644 | $defaultNamespace = $this->namespaces['atom']; 645 | 646 | // Ugly hack to remove the 'atom' value from the prefixes array. 647 | $prefixes = array_flip($prefixes); 648 | unset($prefixes['atom']); 649 | $prefixes = array_flip($prefixes); 650 | } 651 | 652 | // Iterate through every namespace prefix and add it to the element attributes. 653 | foreach ($prefixes as $prefix) { 654 | if (!isset($this->namespaces[$prefix])) 655 | die('Unknown XML namespace prefix: \'' . $prefix . '\'. Use the addNamespace method to add support for this prefix.'); 656 | else 657 | $attributes['xmlns:' . $prefix] = $this->namespaces[$prefix]; 658 | } 659 | 660 | // Include default namepsace, if required 661 | if (!empty($defaultNamespace)) 662 | $attributes['xmlns'] = $defaultNamespace; 663 | 664 | $out .= $this->makeNode($tagName, '', $attributes, true); 665 | 666 | return $out; 667 | } 668 | 669 | /** 670 | * Closes the open tags at the end of file 671 | * 672 | * @access private 673 | * @return string The XML footer of the feed. 674 | */ 675 | private function makeFooter() 676 | { 677 | if ($this->version == Feed::RSS2) { 678 | return '' . PHP_EOL . ''; 679 | } elseif ($this->version == Feed::RSS1) { 680 | return ''; 681 | } elseif ($this->version == Feed::ATOM) { 682 | return ''; 683 | } 684 | } 685 | 686 | /** 687 | * Creates a single node in XML format 688 | * 689 | * @access private 690 | * @param string name of the tag 691 | * @param mixed tag value as string or array of nested tags in 'tagName' => 'tagValue' format 692 | * @param array Attributes (if any) in 'attrName' => 'attrValue' format 693 | * @param string True if the end tag should be omitted. Defaults to false. 694 | * @return string formatted xml tag 695 | */ 696 | private function makeNode($tagName, $tagContent, $attributes = null, $omitEndTag = false) 697 | { 698 | $nodeText = ''; 699 | $attrText = ''; 700 | 701 | if (is_array($attributes) && count($attributes) > 0) { 702 | foreach ($attributes as $key => $value) { 703 | $value = self::utf8_for_xml($value); 704 | $value = htmlspecialchars($value); 705 | $attrText .= " $key=\"$value\""; 706 | } 707 | } 708 | 709 | if (is_array($tagContent) && $this->version == Feed::RSS1) { 710 | $attrText = ' rdf:parseType="Resource"'; 711 | } 712 | 713 | $attrText .= (in_array($tagName, $this->CDATAEncoding) && $this->version == Feed::ATOM) ? ' type="html"' : ''; 714 | $nodeText .= "<{$tagName}{$attrText}>"; 715 | $nodeText .= (in_array($tagName, $this->CDATAEncoding)) ? ' $value) { 719 | $nodeText .= $this->makeNode($key, $value); 720 | } 721 | } else { 722 | $tagContent = self::utf8_for_xml($tagContent); 723 | $nodeText .= (in_array($tagName, $this->CDATAEncoding)) ? $this->sanitizeCDATA($tagContent) : htmlspecialchars($tagContent); 724 | } 725 | 726 | $nodeText .= (in_array($tagName, $this->CDATAEncoding)) ? ']]>' : ''; 727 | 728 | if (!$omitEndTag) 729 | $nodeText .= ""; 730 | 731 | $nodeText .= PHP_EOL; 732 | 733 | return $nodeText; 734 | } 735 | 736 | /** 737 | * Make the channels. 738 | * 739 | * @access private 740 | * @return string The feed header as XML containing all the feed metadata. 741 | */ 742 | private function makeChannels() 743 | { 744 | $out = ''; 745 | 746 | //Start channel tag 747 | switch ($this->version) { 748 | case Feed::RSS2: 749 | $out .= '' . PHP_EOL; 750 | break; 751 | case Feed::RSS1: 752 | $out .= (isset($this->data['ChannelAbout']))? "data['ChannelAbout']}\">" : "channels['link']}\">"; 753 | break; 754 | } 755 | 756 | //Print Items of channel 757 | foreach ($this->channels as $key => $value) { 758 | // In ATOM feeds, strip all ATOM namespace prefixes from the tag name. They are not needed here, 759 | // because the ATOM namespace name is set as default namespace. 760 | if ($this->version == Feed::ATOM && strncmp($key, 'atom', 4) == 0) { 761 | $key = substr($key, 5); 762 | } 763 | 764 | // The channel element can occur multiple times, when the key 'content' is not in the array. 765 | if (!isset($value['content'])) { 766 | // If this is the case, iterate through the array with the multiple elements. 767 | foreach ($value as $singleElement) { 768 | $out .= $this->makeNode($key, $singleElement['content'], $singleElement['attributes']); 769 | } 770 | } else { 771 | $out .= $this->makeNode($key, $value['content'], $value['attributes']); 772 | } 773 | } 774 | 775 | if ($this->version == Feed::RSS1) { 776 | //RSS 1.0 have special tag with channel 777 | $out .= "" . PHP_EOL . "" . PHP_EOL; 778 | foreach ($this->items as $item) { 779 | $thisItems = $item->getElements(); 780 | $out .= "" . PHP_EOL; 781 | } 782 | $out .= "" . PHP_EOL . "" . PHP_EOL . "" . PHP_EOL; 783 | } else if ($this->version == Feed::ATOM) { 784 | // ATOM feeds have a unique feed ID. This is generated from the 'link' channel element. 785 | $out .= $this->makeNode('id', Feed::uuid($this->channels['link']['attributes']['href'], 'urn:uuid:')); 786 | } 787 | 788 | return $out; 789 | } 790 | 791 | /** 792 | * Prints formatted feed items 793 | * 794 | * @access private 795 | * @return string The XML of every feed item. 796 | */ 797 | private function makeItems() 798 | { 799 | $out = ''; 800 | 801 | foreach ($this->items as $item) { 802 | $thisItems = $item->getElements(); 803 | 804 | // the argument is printed as rdf:about attribute of item in rss 1.0 805 | $out .= $this->startItem($thisItems['link']['content']); 806 | 807 | foreach ($thisItems as $feedItem) { 808 | $name = $feedItem['name']; 809 | 810 | // Strip all ATOM namespace prefixes from tags when feed is an ATOM feed. 811 | // Not needed here, because the ATOM namespace name is used as default namespace. 812 | if ($this->version == Feed::ATOM && strncmp($name, 'atom', 4) == 0) 813 | $name = substr($name, 5); 814 | 815 | $out .= $this->makeNode($name, $feedItem['content'], $feedItem['attributes']); 816 | } 817 | $out .= $this->endItem(); 818 | } 819 | 820 | return $out; 821 | } 822 | 823 | /** 824 | * Make the starting tag of channels 825 | * 826 | * @access private 827 | * @param string The vale of about tag which is used for RSS 1.0 only. 828 | * @return string The starting XML tag of an feed item. 829 | */ 830 | private function startItem($about = false) 831 | { 832 | $out = ''; 833 | 834 | if ($this->version == Feed::RSS2) { 835 | $out .= '' . PHP_EOL; 836 | } elseif ($this->version == Feed::RSS1) { 837 | if ($about) { 838 | $out .= "" . PHP_EOL; 839 | } else { 840 | throw new \Exception("link element is not set - It's required for RSS 1.0 to be used as the about attribute of the item tag."); 841 | } 842 | } elseif ($this->version == Feed::ATOM) { 843 | $out .= "" . PHP_EOL; 844 | } 845 | 846 | return $out; 847 | } 848 | 849 | /** 850 | * Closes feed item tag 851 | * 852 | * @access private 853 | * @return string The ending XML tag of an feed item. 854 | */ 855 | private function endItem() 856 | { 857 | if ($this->version == Feed::RSS2 || $this->version == Feed::RSS1) { 858 | return '' . PHP_EOL; 859 | } elseif ($this->version == Feed::ATOM) { 860 | return '' . PHP_EOL; 861 | } 862 | } 863 | 864 | /** 865 | * Sanitizes data which will be later on returned as CDATA in the feed. 866 | * 867 | * A "]]>" respectively "", "]]>", $text); 877 | $text = str_replace("textElements($text); 27 | 28 | # convert to markup 29 | $markup = $this->elements($Elements); 30 | 31 | # trim line breaks 32 | $markup = trim($markup, "\n"); 33 | 34 | return $markup; 35 | } 36 | 37 | protected function textElements($text) 38 | { 39 | # make sure no definitions are set 40 | $this->DefinitionData = array(); 41 | 42 | # standardize line breaks 43 | $text = str_replace(array("\r\n", "\r"), "\n", $text); 44 | 45 | # remove surrounding line breaks 46 | $text = trim($text, "\n"); 47 | 48 | # split text into lines 49 | $lines = explode("\n", $text); 50 | 51 | # iterate through lines to identify blocks 52 | return $this->linesElements($lines); 53 | } 54 | 55 | # 56 | # Setters 57 | # 58 | 59 | function setBreaksEnabled($breaksEnabled) 60 | { 61 | $this->breaksEnabled = $breaksEnabled; 62 | 63 | return $this; 64 | } 65 | 66 | protected $breaksEnabled; 67 | 68 | function setMarkupEscaped($markupEscaped) 69 | { 70 | $this->markupEscaped = $markupEscaped; 71 | 72 | return $this; 73 | } 74 | 75 | protected $markupEscaped; 76 | 77 | function setUrlsLinked($urlsLinked) 78 | { 79 | $this->urlsLinked = $urlsLinked; 80 | 81 | return $this; 82 | } 83 | 84 | protected $urlsLinked = true; 85 | 86 | function setSafeMode($safeMode) 87 | { 88 | $this->safeMode = (bool) $safeMode; 89 | 90 | return $this; 91 | } 92 | 93 | protected $safeMode; 94 | 95 | function setStrictMode($strictMode) 96 | { 97 | $this->strictMode = (bool) $strictMode; 98 | 99 | return $this; 100 | } 101 | 102 | protected $strictMode; 103 | 104 | protected $safeLinksWhitelist = array( 105 | 'http://', 106 | 'https://', 107 | 'ftp://', 108 | 'ftps://', 109 | 'mailto:', 110 | 'tel:', 111 | 'data:image/png;base64,', 112 | 'data:image/gif;base64,', 113 | 'data:image/jpeg;base64,', 114 | 'irc:', 115 | 'ircs:', 116 | 'git:', 117 | 'ssh:', 118 | 'news:', 119 | 'steam:', 120 | ); 121 | 122 | # 123 | # Lines 124 | # 125 | 126 | protected $BlockTypes = array( 127 | '#' => array('Header'), 128 | '*' => array('Rule', 'List'), 129 | '+' => array('List'), 130 | '-' => array('SetextHeader', 'Table', 'Rule', 'List'), 131 | '0' => array('List'), 132 | '1' => array('List'), 133 | '2' => array('List'), 134 | '3' => array('List'), 135 | '4' => array('List'), 136 | '5' => array('List'), 137 | '6' => array('List'), 138 | '7' => array('List'), 139 | '8' => array('List'), 140 | '9' => array('List'), 141 | ':' => array('Table'), 142 | '<' => array('Comment', 'Markup'), 143 | '=' => array('SetextHeader'), 144 | '>' => array('Quote'), 145 | '[' => array('Reference'), 146 | '_' => array('Rule'), 147 | '`' => array('FencedCode'), 148 | '|' => array('Table'), 149 | '~' => array('FencedCode'), 150 | ); 151 | 152 | # ~ 153 | 154 | protected $unmarkedBlockTypes = array( 155 | 'Code', 156 | ); 157 | 158 | # 159 | # Blocks 160 | # 161 | 162 | protected function lines(array $lines) 163 | { 164 | return $this->elements($this->linesElements($lines)); 165 | } 166 | 167 | protected function linesElements(array $lines) 168 | { 169 | $Elements = array(); 170 | $CurrentBlock = null; 171 | 172 | foreach ($lines as $line) 173 | { 174 | if (chop($line) === '') 175 | { 176 | if (isset($CurrentBlock)) 177 | { 178 | $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) 179 | ? $CurrentBlock['interrupted'] + 1 : 1 180 | ); 181 | } 182 | 183 | continue; 184 | } 185 | 186 | while (($beforeTab = strstr($line, "\t", true)) !== false) 187 | { 188 | $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; 189 | 190 | $line = $beforeTab 191 | . str_repeat(' ', $shortage) 192 | . substr($line, strlen($beforeTab) + 1) 193 | ; 194 | } 195 | 196 | $indent = strspn($line, ' '); 197 | 198 | $text = $indent > 0 ? substr($line, $indent) : $line; 199 | 200 | # ~ 201 | 202 | $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); 203 | 204 | # ~ 205 | 206 | if (isset($CurrentBlock['continuable'])) 207 | { 208 | $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; 209 | $Block = $this->$methodName($Line, $CurrentBlock); 210 | 211 | if (isset($Block)) 212 | { 213 | $CurrentBlock = $Block; 214 | 215 | continue; 216 | } 217 | else 218 | { 219 | if ($this->isBlockCompletable($CurrentBlock['type'])) 220 | { 221 | $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; 222 | $CurrentBlock = $this->$methodName($CurrentBlock); 223 | } 224 | } 225 | } 226 | 227 | # ~ 228 | 229 | $marker = $text[0]; 230 | 231 | # ~ 232 | 233 | $blockTypes = $this->unmarkedBlockTypes; 234 | 235 | if (isset($this->BlockTypes[$marker])) 236 | { 237 | foreach ($this->BlockTypes[$marker] as $blockType) 238 | { 239 | $blockTypes []= $blockType; 240 | } 241 | } 242 | 243 | # 244 | # ~ 245 | 246 | foreach ($blockTypes as $blockType) 247 | { 248 | $Block = $this->{"block$blockType"}($Line, $CurrentBlock); 249 | 250 | if (isset($Block)) 251 | { 252 | $Block['type'] = $blockType; 253 | 254 | if ( ! isset($Block['identified'])) 255 | { 256 | if (isset($CurrentBlock)) 257 | { 258 | $Elements[] = $this->extractElement($CurrentBlock); 259 | } 260 | 261 | $Block['identified'] = true; 262 | } 263 | 264 | if ($this->isBlockContinuable($blockType)) 265 | { 266 | $Block['continuable'] = true; 267 | } 268 | 269 | $CurrentBlock = $Block; 270 | 271 | continue 2; 272 | } 273 | } 274 | 275 | # ~ 276 | 277 | if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') 278 | { 279 | $Block = $this->paragraphContinue($Line, $CurrentBlock); 280 | } 281 | 282 | if (isset($Block)) 283 | { 284 | $CurrentBlock = $Block; 285 | } 286 | else 287 | { 288 | if (isset($CurrentBlock)) 289 | { 290 | $Elements[] = $this->extractElement($CurrentBlock); 291 | } 292 | 293 | $CurrentBlock = $this->paragraph($Line); 294 | 295 | $CurrentBlock['identified'] = true; 296 | } 297 | } 298 | 299 | # ~ 300 | 301 | if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) 302 | { 303 | $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; 304 | $CurrentBlock = $this->$methodName($CurrentBlock); 305 | } 306 | 307 | # ~ 308 | 309 | if (isset($CurrentBlock)) 310 | { 311 | $Elements[] = $this->extractElement($CurrentBlock); 312 | } 313 | 314 | # ~ 315 | 316 | return $Elements; 317 | } 318 | 319 | protected function extractElement(array $Component) 320 | { 321 | if ( ! isset($Component['element'])) 322 | { 323 | if (isset($Component['markup'])) 324 | { 325 | $Component['element'] = array('rawHtml' => $Component['markup']); 326 | } 327 | elseif (isset($Component['hidden'])) 328 | { 329 | $Component['element'] = array(); 330 | } 331 | } 332 | 333 | return $Component['element']; 334 | } 335 | 336 | protected function isBlockContinuable($Type) 337 | { 338 | return method_exists($this, 'block' . $Type . 'Continue'); 339 | } 340 | 341 | protected function isBlockCompletable($Type) 342 | { 343 | return method_exists($this, 'block' . $Type . 'Complete'); 344 | } 345 | 346 | # 347 | # Code 348 | 349 | protected function blockCode($Line, $Block = null) 350 | { 351 | if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) 352 | { 353 | return; 354 | } 355 | 356 | if ($Line['indent'] >= 4) 357 | { 358 | $text = substr($Line['body'], 4); 359 | 360 | $Block = array( 361 | 'element' => array( 362 | 'name' => 'pre', 363 | 'element' => array( 364 | 'name' => 'code', 365 | 'text' => $text, 366 | ), 367 | ), 368 | ); 369 | 370 | return $Block; 371 | } 372 | } 373 | 374 | protected function blockCodeContinue($Line, $Block) 375 | { 376 | if ($Line['indent'] >= 4) 377 | { 378 | if (isset($Block['interrupted'])) 379 | { 380 | $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); 381 | 382 | unset($Block['interrupted']); 383 | } 384 | 385 | $Block['element']['element']['text'] .= "\n"; 386 | 387 | $text = substr($Line['body'], 4); 388 | 389 | $Block['element']['element']['text'] .= $text; 390 | 391 | return $Block; 392 | } 393 | } 394 | 395 | protected function blockCodeComplete($Block) 396 | { 397 | return $Block; 398 | } 399 | 400 | # 401 | # Comment 402 | 403 | protected function blockComment($Line) 404 | { 405 | if ($this->markupEscaped or $this->safeMode) 406 | { 407 | return; 408 | } 409 | 410 | if (strpos($Line['text'], '') !== false) 420 | { 421 | $Block['closed'] = true; 422 | } 423 | 424 | return $Block; 425 | } 426 | } 427 | 428 | protected function blockCommentContinue($Line, array $Block) 429 | { 430 | if (isset($Block['closed'])) 431 | { 432 | return; 433 | } 434 | 435 | $Block['element']['rawHtml'] .= "\n" . $Line['body']; 436 | 437 | if (strpos($Line['text'], '-->') !== false) 438 | { 439 | $Block['closed'] = true; 440 | } 441 | 442 | return $Block; 443 | } 444 | 445 | # 446 | # Fenced Code 447 | 448 | protected function blockFencedCode($Line) 449 | { 450 | $marker = $Line['text'][0]; 451 | 452 | $openerLength = strspn($Line['text'], $marker); 453 | 454 | if ($openerLength < 3) 455 | { 456 | return; 457 | } 458 | 459 | $infostring = trim(substr($Line['text'], $openerLength), "\t "); 460 | 461 | if (strpos($infostring, '`') !== false) 462 | { 463 | return; 464 | } 465 | 466 | $Element = array( 467 | 'name' => 'code', 468 | 'text' => '', 469 | ); 470 | 471 | if ($infostring !== '') 472 | { 473 | /** 474 | * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes 475 | * Every HTML element may have a class attribute specified. 476 | * The attribute, if specified, must have a value that is a set 477 | * of space-separated tokens representing the various classes 478 | * that the element belongs to. 479 | * [...] 480 | * The space characters, for the purposes of this specification, 481 | * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), 482 | * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and 483 | * U+000D CARRIAGE RETURN (CR). 484 | */ 485 | $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); 486 | 487 | $Element['attributes'] = array('class' => "language-$language"); 488 | } 489 | 490 | $Block = array( 491 | 'char' => $marker, 492 | 'openerLength' => $openerLength, 493 | 'element' => array( 494 | 'name' => 'pre', 495 | 'element' => $Element, 496 | ), 497 | ); 498 | 499 | return $Block; 500 | } 501 | 502 | protected function blockFencedCodeContinue($Line, $Block) 503 | { 504 | if (isset($Block['complete'])) 505 | { 506 | return; 507 | } 508 | 509 | if (isset($Block['interrupted'])) 510 | { 511 | $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); 512 | 513 | unset($Block['interrupted']); 514 | } 515 | 516 | if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] 517 | and chop(substr($Line['text'], $len), ' ') === '' 518 | ) { 519 | $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); 520 | 521 | $Block['complete'] = true; 522 | 523 | return $Block; 524 | } 525 | 526 | $Block['element']['element']['text'] .= "\n" . $Line['body']; 527 | 528 | return $Block; 529 | } 530 | 531 | protected function blockFencedCodeComplete($Block) 532 | { 533 | return $Block; 534 | } 535 | 536 | # 537 | # Header 538 | 539 | protected function blockHeader($Line) 540 | { 541 | $level = strspn($Line['text'], '#'); 542 | 543 | if ($level > 6) 544 | { 545 | return; 546 | } 547 | 548 | $text = trim($Line['text'], '#'); 549 | 550 | if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') 551 | { 552 | return; 553 | } 554 | 555 | $text = trim($text, ' '); 556 | 557 | $Block = array( 558 | 'element' => array( 559 | 'name' => 'h' . $level, 560 | 'handler' => array( 561 | 'function' => 'lineElements', 562 | 'argument' => $text, 563 | 'destination' => 'elements', 564 | ) 565 | ), 566 | ); 567 | 568 | return $Block; 569 | } 570 | 571 | # 572 | # List 573 | 574 | protected function blockList($Line, ?array $CurrentBlock = null) 575 | { 576 | list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); 577 | 578 | if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) 579 | { 580 | $contentIndent = strlen($matches[2]); 581 | 582 | if ($contentIndent >= 5) 583 | { 584 | $contentIndent -= 1; 585 | $matches[1] = substr($matches[1], 0, -$contentIndent); 586 | $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; 587 | } 588 | elseif ($contentIndent === 0) 589 | { 590 | $matches[1] .= ' '; 591 | } 592 | 593 | $markerWithoutWhitespace = strstr($matches[1], ' ', true); 594 | 595 | $Block = array( 596 | 'indent' => $Line['indent'], 597 | 'pattern' => $pattern, 598 | 'data' => array( 599 | 'type' => $name, 600 | 'marker' => $matches[1], 601 | 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), 602 | ), 603 | 'element' => array( 604 | 'name' => $name, 605 | 'elements' => array(), 606 | ), 607 | ); 608 | $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); 609 | 610 | if ($name === 'ol') 611 | { 612 | $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; 613 | 614 | if ($listStart !== '1') 615 | { 616 | if ( 617 | isset($CurrentBlock) 618 | and $CurrentBlock['type'] === 'Paragraph' 619 | and ! isset($CurrentBlock['interrupted']) 620 | ) { 621 | return; 622 | } 623 | 624 | $Block['element']['attributes'] = array('start' => $listStart); 625 | } 626 | } 627 | 628 | $Block['li'] = array( 629 | 'name' => 'li', 630 | 'handler' => array( 631 | 'function' => 'li', 632 | 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), 633 | 'destination' => 'elements' 634 | ) 635 | ); 636 | 637 | $Block['element']['elements'] []= & $Block['li']; 638 | 639 | return $Block; 640 | } 641 | } 642 | 643 | protected function blockListContinue($Line, array $Block) 644 | { 645 | if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) 646 | { 647 | return null; 648 | } 649 | 650 | $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); 651 | 652 | if ($Line['indent'] < $requiredIndent 653 | and ( 654 | ( 655 | $Block['data']['type'] === 'ol' 656 | and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) 657 | ) or ( 658 | $Block['data']['type'] === 'ul' 659 | and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) 660 | ) 661 | ) 662 | ) { 663 | if (isset($Block['interrupted'])) 664 | { 665 | $Block['li']['handler']['argument'] []= ''; 666 | 667 | $Block['loose'] = true; 668 | 669 | unset($Block['interrupted']); 670 | } 671 | 672 | unset($Block['li']); 673 | 674 | $text = isset($matches[1]) ? $matches[1] : ''; 675 | 676 | $Block['indent'] = $Line['indent']; 677 | 678 | $Block['li'] = array( 679 | 'name' => 'li', 680 | 'handler' => array( 681 | 'function' => 'li', 682 | 'argument' => array($text), 683 | 'destination' => 'elements' 684 | ) 685 | ); 686 | 687 | $Block['element']['elements'] []= & $Block['li']; 688 | 689 | return $Block; 690 | } 691 | elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) 692 | { 693 | return null; 694 | } 695 | 696 | if ($Line['text'][0] === '[' and $this->blockReference($Line)) 697 | { 698 | return $Block; 699 | } 700 | 701 | if ($Line['indent'] >= $requiredIndent) 702 | { 703 | if (isset($Block['interrupted'])) 704 | { 705 | $Block['li']['handler']['argument'] []= ''; 706 | 707 | $Block['loose'] = true; 708 | 709 | unset($Block['interrupted']); 710 | } 711 | 712 | $text = substr($Line['body'], $requiredIndent); 713 | 714 | $Block['li']['handler']['argument'] []= $text; 715 | 716 | return $Block; 717 | } 718 | 719 | if ( ! isset($Block['interrupted'])) 720 | { 721 | $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); 722 | 723 | $Block['li']['handler']['argument'] []= $text; 724 | 725 | return $Block; 726 | } 727 | } 728 | 729 | protected function blockListComplete(array $Block) 730 | { 731 | if (isset($Block['loose'])) 732 | { 733 | foreach ($Block['element']['elements'] as &$li) 734 | { 735 | if (end($li['handler']['argument']) !== '') 736 | { 737 | $li['handler']['argument'] []= ''; 738 | } 739 | } 740 | } 741 | 742 | return $Block; 743 | } 744 | 745 | # 746 | # Quote 747 | 748 | protected function blockQuote($Line) 749 | { 750 | if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) 751 | { 752 | $Block = array( 753 | 'element' => array( 754 | 'name' => 'blockquote', 755 | 'handler' => array( 756 | 'function' => 'linesElements', 757 | 'argument' => (array) $matches[1], 758 | 'destination' => 'elements', 759 | ) 760 | ), 761 | ); 762 | 763 | return $Block; 764 | } 765 | } 766 | 767 | protected function blockQuoteContinue($Line, array $Block) 768 | { 769 | if (isset($Block['interrupted'])) 770 | { 771 | return; 772 | } 773 | 774 | if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) 775 | { 776 | $Block['element']['handler']['argument'] []= $matches[1]; 777 | 778 | return $Block; 779 | } 780 | 781 | if ( ! isset($Block['interrupted'])) 782 | { 783 | $Block['element']['handler']['argument'] []= $Line['text']; 784 | 785 | return $Block; 786 | } 787 | } 788 | 789 | # 790 | # Rule 791 | 792 | protected function blockRule($Line) 793 | { 794 | $marker = $Line['text'][0]; 795 | 796 | if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') 797 | { 798 | $Block = array( 799 | 'element' => array( 800 | 'name' => 'hr', 801 | ), 802 | ); 803 | 804 | return $Block; 805 | } 806 | } 807 | 808 | # 809 | # Setext 810 | 811 | protected function blockSetextHeader($Line, ?array $Block = null) 812 | { 813 | if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) 814 | { 815 | return; 816 | } 817 | 818 | if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') 819 | { 820 | $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; 821 | 822 | return $Block; 823 | } 824 | } 825 | 826 | # 827 | # Markup 828 | 829 | protected function blockMarkup($Line) 830 | { 831 | if ($this->markupEscaped or $this->safeMode) 832 | { 833 | return; 834 | } 835 | 836 | if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) 837 | { 838 | $element = strtolower($matches[1]); 839 | 840 | if (in_array($element, $this->textLevelElements)) 841 | { 842 | return; 843 | } 844 | 845 | $Block = array( 846 | 'name' => $matches[1], 847 | 'element' => array( 848 | 'rawHtml' => $Line['text'], 849 | 'autobreak' => true, 850 | ), 851 | ); 852 | 853 | return $Block; 854 | } 855 | } 856 | 857 | protected function blockMarkupContinue($Line, array $Block) 858 | { 859 | if (isset($Block['closed']) or isset($Block['interrupted'])) 860 | { 861 | return; 862 | } 863 | 864 | $Block['element']['rawHtml'] .= "\n" . $Line['body']; 865 | 866 | return $Block; 867 | } 868 | 869 | # 870 | # Reference 871 | 872 | protected function blockReference($Line) 873 | { 874 | if (strpos($Line['text'], ']') !== false 875 | and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) 876 | ) { 877 | $id = strtolower($matches[1]); 878 | 879 | $Data = array( 880 | 'url' => $matches[2], 881 | 'title' => isset($matches[3]) ? $matches[3] : null, 882 | ); 883 | 884 | $this->DefinitionData['Reference'][$id] = $Data; 885 | 886 | $Block = array( 887 | 'element' => array(), 888 | ); 889 | 890 | return $Block; 891 | } 892 | } 893 | 894 | # 895 | # Table 896 | 897 | protected function blockTable($Line, ?array $Block = null) 898 | { 899 | if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) 900 | { 901 | return; 902 | } 903 | 904 | if ( 905 | strpos($Block['element']['handler']['argument'], '|') === false 906 | and strpos($Line['text'], '|') === false 907 | and strpos($Line['text'], ':') === false 908 | or strpos($Block['element']['handler']['argument'], "\n") !== false 909 | ) { 910 | return; 911 | } 912 | 913 | if (chop($Line['text'], ' -:|') !== '') 914 | { 915 | return; 916 | } 917 | 918 | $alignments = array(); 919 | 920 | $divider = $Line['text']; 921 | 922 | $divider = trim($divider); 923 | $divider = trim($divider, '|'); 924 | 925 | $dividerCells = explode('|', $divider); 926 | 927 | foreach ($dividerCells as $dividerCell) 928 | { 929 | $dividerCell = trim($dividerCell); 930 | 931 | if ($dividerCell === '') 932 | { 933 | return; 934 | } 935 | 936 | $alignment = null; 937 | 938 | if ($dividerCell[0] === ':') 939 | { 940 | $alignment = 'left'; 941 | } 942 | 943 | if (substr($dividerCell, - 1) === ':') 944 | { 945 | $alignment = $alignment === 'left' ? 'center' : 'right'; 946 | } 947 | 948 | $alignments []= $alignment; 949 | } 950 | 951 | # ~ 952 | 953 | $HeaderElements = array(); 954 | 955 | $header = $Block['element']['handler']['argument']; 956 | 957 | $header = trim($header); 958 | $header = trim($header, '|'); 959 | 960 | $headerCells = explode('|', $header); 961 | 962 | if (count($headerCells) !== count($alignments)) 963 | { 964 | return; 965 | } 966 | 967 | foreach ($headerCells as $index => $headerCell) 968 | { 969 | $headerCell = trim($headerCell); 970 | 971 | $HeaderElement = array( 972 | 'name' => 'th', 973 | 'handler' => array( 974 | 'function' => 'lineElements', 975 | 'argument' => $headerCell, 976 | 'destination' => 'elements', 977 | ) 978 | ); 979 | 980 | if (isset($alignments[$index])) 981 | { 982 | $alignment = $alignments[$index]; 983 | 984 | $HeaderElement['attributes'] = array( 985 | 'style' => "text-align: $alignment;", 986 | ); 987 | } 988 | 989 | $HeaderElements []= $HeaderElement; 990 | } 991 | 992 | # ~ 993 | 994 | $Block = array( 995 | 'alignments' => $alignments, 996 | 'identified' => true, 997 | 'element' => array( 998 | 'name' => 'table', 999 | 'elements' => array(), 1000 | ), 1001 | ); 1002 | 1003 | $Block['element']['elements'] []= array( 1004 | 'name' => 'thead', 1005 | ); 1006 | 1007 | $Block['element']['elements'] []= array( 1008 | 'name' => 'tbody', 1009 | 'elements' => array(), 1010 | ); 1011 | 1012 | $Block['element']['elements'][0]['elements'] []= array( 1013 | 'name' => 'tr', 1014 | 'elements' => $HeaderElements, 1015 | ); 1016 | 1017 | return $Block; 1018 | } 1019 | 1020 | protected function blockTableContinue($Line, array $Block) 1021 | { 1022 | if (isset($Block['interrupted'])) 1023 | { 1024 | return; 1025 | } 1026 | 1027 | if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) 1028 | { 1029 | $Elements = array(); 1030 | 1031 | $row = $Line['text']; 1032 | 1033 | $row = trim($row); 1034 | $row = trim($row, '|'); 1035 | 1036 | preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); 1037 | 1038 | $cells = array_slice($matches[0], 0, count($Block['alignments'])); 1039 | 1040 | foreach ($cells as $index => $cell) 1041 | { 1042 | $cell = trim($cell); 1043 | 1044 | $Element = array( 1045 | 'name' => 'td', 1046 | 'handler' => array( 1047 | 'function' => 'lineElements', 1048 | 'argument' => $cell, 1049 | 'destination' => 'elements', 1050 | ) 1051 | ); 1052 | 1053 | if (isset($Block['alignments'][$index])) 1054 | { 1055 | $Element['attributes'] = array( 1056 | 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', 1057 | ); 1058 | } 1059 | 1060 | $Elements []= $Element; 1061 | } 1062 | 1063 | $Element = array( 1064 | 'name' => 'tr', 1065 | 'elements' => $Elements, 1066 | ); 1067 | 1068 | $Block['element']['elements'][1]['elements'] []= $Element; 1069 | 1070 | return $Block; 1071 | } 1072 | } 1073 | 1074 | # 1075 | # ~ 1076 | # 1077 | 1078 | protected function paragraph($Line) 1079 | { 1080 | return array( 1081 | 'type' => 'Paragraph', 1082 | 'element' => array( 1083 | 'name' => 'p', 1084 | 'handler' => array( 1085 | 'function' => 'lineElements', 1086 | 'argument' => $Line['text'], 1087 | 'destination' => 'elements', 1088 | ), 1089 | ), 1090 | ); 1091 | } 1092 | 1093 | protected function paragraphContinue($Line, array $Block) 1094 | { 1095 | if (isset($Block['interrupted'])) 1096 | { 1097 | return; 1098 | } 1099 | 1100 | $Block['element']['handler']['argument'] .= "\n".$Line['text']; 1101 | 1102 | return $Block; 1103 | } 1104 | 1105 | # 1106 | # Inline Elements 1107 | # 1108 | 1109 | protected $InlineTypes = array( 1110 | '!' => array('Image'), 1111 | '&' => array('SpecialCharacter'), 1112 | '*' => array('Emphasis'), 1113 | ':' => array('Url'), 1114 | '<' => array('UrlTag', 'EmailTag', 'Markup'), 1115 | '[' => array('Link'), 1116 | '_' => array('Emphasis'), 1117 | '`' => array('Code'), 1118 | '~' => array('Strikethrough'), 1119 | '\\' => array('EscapeSequence'), 1120 | ); 1121 | 1122 | # ~ 1123 | 1124 | protected $inlineMarkerList = '!*_&[:<`~\\'; 1125 | 1126 | # 1127 | # ~ 1128 | # 1129 | 1130 | public function line($text, $nonNestables = array()) 1131 | { 1132 | return $this->elements($this->lineElements($text, $nonNestables)); 1133 | } 1134 | 1135 | protected function lineElements($text, $nonNestables = array()) 1136 | { 1137 | # standardize line breaks 1138 | $text = str_replace(array("\r\n", "\r"), "\n", $text); 1139 | 1140 | $Elements = array(); 1141 | 1142 | $nonNestables = (empty($nonNestables) 1143 | ? array() 1144 | : array_combine($nonNestables, $nonNestables) 1145 | ); 1146 | 1147 | # $excerpt is based on the first occurrence of a marker 1148 | 1149 | while ($excerpt = strpbrk($text, $this->inlineMarkerList)) 1150 | { 1151 | $marker = $excerpt[0]; 1152 | 1153 | $markerPosition = strlen($text) - strlen($excerpt); 1154 | 1155 | $Excerpt = array('text' => $excerpt, 'context' => $text); 1156 | 1157 | foreach ($this->InlineTypes[$marker] as $inlineType) 1158 | { 1159 | # check to see if the current inline type is nestable in the current context 1160 | 1161 | if (isset($nonNestables[$inlineType])) 1162 | { 1163 | continue; 1164 | } 1165 | 1166 | $Inline = $this->{"inline$inlineType"}($Excerpt); 1167 | 1168 | if ( ! isset($Inline)) 1169 | { 1170 | continue; 1171 | } 1172 | 1173 | # makes sure that the inline belongs to "our" marker 1174 | 1175 | if (isset($Inline['position']) and $Inline['position'] > $markerPosition) 1176 | { 1177 | continue; 1178 | } 1179 | 1180 | # sets a default inline position 1181 | 1182 | if ( ! isset($Inline['position'])) 1183 | { 1184 | $Inline['position'] = $markerPosition; 1185 | } 1186 | 1187 | # cause the new element to 'inherit' our non nestables 1188 | 1189 | 1190 | $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) 1191 | ? array_merge($Inline['element']['nonNestables'], $nonNestables) 1192 | : $nonNestables 1193 | ; 1194 | 1195 | # the text that comes before the inline 1196 | $unmarkedText = substr($text, 0, $Inline['position']); 1197 | 1198 | # compile the unmarked text 1199 | $InlineText = $this->inlineText($unmarkedText); 1200 | $Elements[] = $InlineText['element']; 1201 | 1202 | # compile the inline 1203 | $Elements[] = $this->extractElement($Inline); 1204 | 1205 | # remove the examined text 1206 | $text = substr($text, $Inline['position'] + $Inline['extent']); 1207 | 1208 | continue 2; 1209 | } 1210 | 1211 | # the marker does not belong to an inline 1212 | 1213 | $unmarkedText = substr($text, 0, $markerPosition + 1); 1214 | 1215 | $InlineText = $this->inlineText($unmarkedText); 1216 | $Elements[] = $InlineText['element']; 1217 | 1218 | $text = substr($text, $markerPosition + 1); 1219 | } 1220 | 1221 | $InlineText = $this->inlineText($text); 1222 | $Elements[] = $InlineText['element']; 1223 | 1224 | foreach ($Elements as &$Element) 1225 | { 1226 | if ( ! isset($Element['autobreak'])) 1227 | { 1228 | $Element['autobreak'] = false; 1229 | } 1230 | } 1231 | 1232 | return $Elements; 1233 | } 1234 | 1235 | # 1236 | # ~ 1237 | # 1238 | 1239 | protected function inlineText($text) 1240 | { 1241 | $Inline = array( 1242 | 'extent' => strlen($text), 1243 | 'element' => array(), 1244 | ); 1245 | 1246 | $Inline['element']['elements'] = self::pregReplaceElements( 1247 | $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', 1248 | array( 1249 | array('name' => 'br'), 1250 | array('text' => "\n"), 1251 | ), 1252 | $text 1253 | ); 1254 | 1255 | return $Inline; 1256 | } 1257 | 1258 | protected function inlineCode($Excerpt) 1259 | { 1260 | $marker = $Excerpt['text'][0]; 1261 | 1262 | if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), 1269 | 'element' => array( 1270 | 'name' => 'code', 1271 | 'text' => $text, 1272 | ), 1273 | ); 1274 | } 1275 | } 1276 | 1277 | protected function inlineEmailTag($Excerpt) 1278 | { 1279 | $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; 1280 | 1281 | $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' 1282 | . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; 1283 | 1284 | if (strpos($Excerpt['text'], '>') !== false 1285 | and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) 1286 | ){ 1287 | $url = $matches[1]; 1288 | 1289 | if ( ! isset($matches[2])) 1290 | { 1291 | $url = "mailto:$url"; 1292 | } 1293 | 1294 | return array( 1295 | 'extent' => strlen($matches[0]), 1296 | 'element' => array( 1297 | 'name' => 'a', 1298 | 'text' => $matches[1], 1299 | 'attributes' => array( 1300 | 'href' => $url, 1301 | ), 1302 | ), 1303 | ); 1304 | } 1305 | } 1306 | 1307 | protected function inlineEmphasis($Excerpt) 1308 | { 1309 | if ( ! isset($Excerpt['text'][1])) 1310 | { 1311 | return; 1312 | } 1313 | 1314 | $marker = $Excerpt['text'][0]; 1315 | 1316 | if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) 1317 | { 1318 | $emphasis = 'strong'; 1319 | } 1320 | elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) 1321 | { 1322 | $emphasis = 'em'; 1323 | } 1324 | else 1325 | { 1326 | return; 1327 | } 1328 | 1329 | return array( 1330 | 'extent' => strlen($matches[0]), 1331 | 'element' => array( 1332 | 'name' => $emphasis, 1333 | 'handler' => array( 1334 | 'function' => 'lineElements', 1335 | 'argument' => $matches[1], 1336 | 'destination' => 'elements', 1337 | ) 1338 | ), 1339 | ); 1340 | } 1341 | 1342 | protected function inlineEscapeSequence($Excerpt) 1343 | { 1344 | if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) 1345 | { 1346 | return array( 1347 | 'element' => array('rawHtml' => $Excerpt['text'][1]), 1348 | 'extent' => 2, 1349 | ); 1350 | } 1351 | } 1352 | 1353 | protected function inlineImage($Excerpt) 1354 | { 1355 | if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') 1356 | { 1357 | return; 1358 | } 1359 | 1360 | $Excerpt['text']= substr($Excerpt['text'], 1); 1361 | 1362 | $Link = $this->inlineLink($Excerpt); 1363 | 1364 | if ($Link === null) 1365 | { 1366 | return; 1367 | } 1368 | 1369 | $Inline = array( 1370 | 'extent' => $Link['extent'] + 1, 1371 | 'element' => array( 1372 | 'name' => 'img', 1373 | 'attributes' => array( 1374 | 'src' => $Link['element']['attributes']['href'], 1375 | 'alt' => $Link['element']['handler']['argument'], 1376 | ), 1377 | 'autobreak' => true, 1378 | ), 1379 | ); 1380 | 1381 | $Inline['element']['attributes'] += $Link['element']['attributes']; 1382 | 1383 | unset($Inline['element']['attributes']['href']); 1384 | 1385 | return $Inline; 1386 | } 1387 | 1388 | protected function inlineLink($Excerpt) 1389 | { 1390 | $Element = array( 1391 | 'name' => 'a', 1392 | 'handler' => array( 1393 | 'function' => 'lineElements', 1394 | 'argument' => null, 1395 | 'destination' => 'elements', 1396 | ), 1397 | 'nonNestables' => array('Url', 'Link'), 1398 | 'attributes' => array( 1399 | 'href' => null, 1400 | 'title' => null, 1401 | ), 1402 | ); 1403 | 1404 | $extent = 0; 1405 | 1406 | $remainder = $Excerpt['text']; 1407 | 1408 | if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) 1409 | { 1410 | $Element['handler']['argument'] = $matches[1]; 1411 | 1412 | $extent += strlen($matches[0]); 1413 | 1414 | $remainder = substr($remainder, $extent); 1415 | } 1416 | else 1417 | { 1418 | return; 1419 | } 1420 | 1421 | if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) 1422 | { 1423 | $Element['attributes']['href'] = $matches[1]; 1424 | 1425 | if (isset($matches[2])) 1426 | { 1427 | $Element['attributes']['title'] = substr($matches[2], 1, - 1); 1428 | } 1429 | 1430 | $extent += strlen($matches[0]); 1431 | } 1432 | else 1433 | { 1434 | if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) 1435 | { 1436 | $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; 1437 | $definition = strtolower($definition); 1438 | 1439 | $extent += strlen($matches[0]); 1440 | } 1441 | else 1442 | { 1443 | $definition = strtolower($Element['handler']['argument']); 1444 | } 1445 | 1446 | if ( ! isset($this->DefinitionData['Reference'][$definition])) 1447 | { 1448 | return; 1449 | } 1450 | 1451 | $Definition = $this->DefinitionData['Reference'][$definition]; 1452 | 1453 | $Element['attributes']['href'] = $Definition['url']; 1454 | $Element['attributes']['title'] = $Definition['title']; 1455 | } 1456 | 1457 | return array( 1458 | 'extent' => $extent, 1459 | 'element' => $Element, 1460 | ); 1461 | } 1462 | 1463 | protected function inlineMarkup($Excerpt) 1464 | { 1465 | if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) 1466 | { 1467 | return; 1468 | } 1469 | 1470 | if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) 1471 | { 1472 | return array( 1473 | 'element' => array('rawHtml' => $matches[0]), 1474 | 'extent' => strlen($matches[0]), 1475 | ); 1476 | } 1477 | 1478 | if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) 1479 | { 1480 | return array( 1481 | 'element' => array('rawHtml' => $matches[0]), 1482 | 'extent' => strlen($matches[0]), 1483 | ); 1484 | } 1485 | 1486 | if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) 1487 | { 1488 | return array( 1489 | 'element' => array('rawHtml' => $matches[0]), 1490 | 'extent' => strlen($matches[0]), 1491 | ); 1492 | } 1493 | } 1494 | 1495 | protected function inlineSpecialCharacter($Excerpt) 1496 | { 1497 | if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false 1498 | and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) 1499 | ) { 1500 | return array( 1501 | 'element' => array('rawHtml' => '&' . $matches[1] . ';'), 1502 | 'extent' => strlen($matches[0]), 1503 | ); 1504 | } 1505 | 1506 | return; 1507 | } 1508 | 1509 | protected function inlineStrikethrough($Excerpt) 1510 | { 1511 | if ( ! isset($Excerpt['text'][1])) 1512 | { 1513 | return; 1514 | } 1515 | 1516 | if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) 1517 | { 1518 | return array( 1519 | 'extent' => strlen($matches[0]), 1520 | 'element' => array( 1521 | 'name' => 'del', 1522 | 'handler' => array( 1523 | 'function' => 'lineElements', 1524 | 'argument' => $matches[1], 1525 | 'destination' => 'elements', 1526 | ) 1527 | ), 1528 | ); 1529 | } 1530 | } 1531 | 1532 | protected function inlineUrl($Excerpt) 1533 | { 1534 | if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') 1535 | { 1536 | return; 1537 | } 1538 | 1539 | if (strpos($Excerpt['context'], 'http') !== false 1540 | and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) 1541 | ) { 1542 | $url = $matches[0][0]; 1543 | 1544 | $Inline = array( 1545 | 'extent' => strlen($matches[0][0]), 1546 | 'position' => $matches[0][1], 1547 | 'element' => array( 1548 | 'name' => 'a', 1549 | 'text' => $url, 1550 | 'attributes' => array( 1551 | 'href' => $url, 1552 | ), 1553 | ), 1554 | ); 1555 | 1556 | return $Inline; 1557 | } 1558 | } 1559 | 1560 | protected function inlineUrlTag($Excerpt) 1561 | { 1562 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) 1563 | { 1564 | $url = $matches[1]; 1565 | 1566 | return array( 1567 | 'extent' => strlen($matches[0]), 1568 | 'element' => array( 1569 | 'name' => 'a', 1570 | 'text' => $url, 1571 | 'attributes' => array( 1572 | 'href' => $url, 1573 | ), 1574 | ), 1575 | ); 1576 | } 1577 | } 1578 | 1579 | # ~ 1580 | 1581 | protected function unmarkedText($text) 1582 | { 1583 | $Inline = $this->inlineText($text); 1584 | return $this->element($Inline['element']); 1585 | } 1586 | 1587 | # 1588 | # Handlers 1589 | # 1590 | 1591 | protected function handle(array $Element) 1592 | { 1593 | if (isset($Element['handler'])) 1594 | { 1595 | if (!isset($Element['nonNestables'])) 1596 | { 1597 | $Element['nonNestables'] = array(); 1598 | } 1599 | 1600 | if (is_string($Element['handler'])) 1601 | { 1602 | $function = $Element['handler']; 1603 | $argument = $Element['text']; 1604 | unset($Element['text']); 1605 | $destination = 'rawHtml'; 1606 | } 1607 | else 1608 | { 1609 | $function = $Element['handler']['function']; 1610 | $argument = $Element['handler']['argument']; 1611 | $destination = $Element['handler']['destination']; 1612 | } 1613 | 1614 | $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); 1615 | 1616 | if ($destination === 'handler') 1617 | { 1618 | $Element = $this->handle($Element); 1619 | } 1620 | 1621 | unset($Element['handler']); 1622 | } 1623 | 1624 | return $Element; 1625 | } 1626 | 1627 | protected function handleElementRecursive(array $Element) 1628 | { 1629 | return $this->elementApplyRecursive(array($this, 'handle'), $Element); 1630 | } 1631 | 1632 | protected function handleElementsRecursive(array $Elements) 1633 | { 1634 | return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); 1635 | } 1636 | 1637 | protected function elementApplyRecursive($closure, array $Element) 1638 | { 1639 | $Element = call_user_func($closure, $Element); 1640 | 1641 | if (isset($Element['elements'])) 1642 | { 1643 | $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); 1644 | } 1645 | elseif (isset($Element['element'])) 1646 | { 1647 | $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); 1648 | } 1649 | 1650 | return $Element; 1651 | } 1652 | 1653 | protected function elementApplyRecursiveDepthFirst($closure, array $Element) 1654 | { 1655 | if (isset($Element['elements'])) 1656 | { 1657 | $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); 1658 | } 1659 | elseif (isset($Element['element'])) 1660 | { 1661 | $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); 1662 | } 1663 | 1664 | $Element = call_user_func($closure, $Element); 1665 | 1666 | return $Element; 1667 | } 1668 | 1669 | protected function elementsApplyRecursive($closure, array $Elements) 1670 | { 1671 | foreach ($Elements as &$Element) 1672 | { 1673 | $Element = $this->elementApplyRecursive($closure, $Element); 1674 | } 1675 | 1676 | return $Elements; 1677 | } 1678 | 1679 | protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) 1680 | { 1681 | foreach ($Elements as &$Element) 1682 | { 1683 | $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); 1684 | } 1685 | 1686 | return $Elements; 1687 | } 1688 | 1689 | protected function element(array $Element) 1690 | { 1691 | if ($this->safeMode) 1692 | { 1693 | $Element = $this->sanitiseElement($Element); 1694 | } 1695 | 1696 | # identity map if element has no handler 1697 | $Element = $this->handle($Element); 1698 | 1699 | $hasName = isset($Element['name']); 1700 | 1701 | $markup = ''; 1702 | 1703 | if ($hasName) 1704 | { 1705 | $markup .= '<' . $Element['name']; 1706 | 1707 | if (isset($Element['attributes'])) 1708 | { 1709 | foreach ($Element['attributes'] as $name => $value) 1710 | { 1711 | if ($value === null) 1712 | { 1713 | continue; 1714 | } 1715 | 1716 | $markup .= " $name=\"".self::escape($value).'"'; 1717 | } 1718 | } 1719 | } 1720 | 1721 | $permitRawHtml = false; 1722 | 1723 | if (isset($Element['text'])) 1724 | { 1725 | $text = $Element['text']; 1726 | } 1727 | // very strongly consider an alternative if you're writing an 1728 | // extension 1729 | elseif (isset($Element['rawHtml'])) 1730 | { 1731 | $text = $Element['rawHtml']; 1732 | 1733 | $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; 1734 | $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; 1735 | } 1736 | 1737 | $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); 1738 | 1739 | if ($hasContent) 1740 | { 1741 | $markup .= $hasName ? '>' : ''; 1742 | 1743 | if (isset($Element['elements'])) 1744 | { 1745 | $markup .= $this->elements($Element['elements']); 1746 | } 1747 | elseif (isset($Element['element'])) 1748 | { 1749 | $markup .= $this->element($Element['element']); 1750 | } 1751 | else 1752 | { 1753 | if (!$permitRawHtml) 1754 | { 1755 | $markup .= self::escape($text, true); 1756 | } 1757 | else 1758 | { 1759 | $markup .= $text; 1760 | } 1761 | } 1762 | 1763 | $markup .= $hasName ? '' : ''; 1764 | } 1765 | elseif ($hasName) 1766 | { 1767 | $markup .= ' />'; 1768 | } 1769 | 1770 | return $markup; 1771 | } 1772 | 1773 | protected function elements(array $Elements) 1774 | { 1775 | $markup = ''; 1776 | 1777 | $autoBreak = true; 1778 | 1779 | foreach ($Elements as $Element) 1780 | { 1781 | if (empty($Element)) 1782 | { 1783 | continue; 1784 | } 1785 | 1786 | $autoBreakNext = (isset($Element['autobreak']) 1787 | ? $Element['autobreak'] : isset($Element['name']) 1788 | ); 1789 | // (autobreak === false) covers both sides of an element 1790 | $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; 1791 | 1792 | $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); 1793 | $autoBreak = $autoBreakNext; 1794 | } 1795 | 1796 | $markup .= $autoBreak ? "\n" : ''; 1797 | 1798 | return $markup; 1799 | } 1800 | 1801 | # ~ 1802 | 1803 | protected function li($lines) 1804 | { 1805 | $Elements = $this->linesElements($lines); 1806 | 1807 | if ( ! in_array('', $lines) 1808 | and isset($Elements[0]) and isset($Elements[0]['name']) 1809 | and $Elements[0]['name'] === 'p' 1810 | ) { 1811 | unset($Elements[0]['name']); 1812 | } 1813 | 1814 | return $Elements; 1815 | } 1816 | 1817 | # 1818 | # AST Convenience 1819 | # 1820 | 1821 | /** 1822 | * Replace occurrences $regexp with $Elements in $text. Return an array of 1823 | * elements representing the replacement. 1824 | */ 1825 | protected static function pregReplaceElements($regexp, $Elements, $text) 1826 | { 1827 | $newElements = array(); 1828 | 1829 | while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) 1830 | { 1831 | $offset = $matches[0][1]; 1832 | $before = substr($text, 0, $offset); 1833 | $after = substr($text, $offset + strlen($matches[0][0])); 1834 | 1835 | $newElements[] = array('text' => $before); 1836 | 1837 | foreach ($Elements as $Element) 1838 | { 1839 | $newElements[] = $Element; 1840 | } 1841 | 1842 | $text = $after; 1843 | } 1844 | 1845 | $newElements[] = array('text' => $text); 1846 | 1847 | return $newElements; 1848 | } 1849 | 1850 | # 1851 | # Deprecated Methods 1852 | # 1853 | 1854 | function parse($text) 1855 | { 1856 | $markup = $this->text($text); 1857 | 1858 | return $markup; 1859 | } 1860 | 1861 | protected function sanitiseElement(array $Element) 1862 | { 1863 | static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; 1864 | static $safeUrlNameToAtt = array( 1865 | 'a' => 'href', 1866 | 'img' => 'src', 1867 | ); 1868 | 1869 | if ( ! isset($Element['name'])) 1870 | { 1871 | unset($Element['attributes']); 1872 | return $Element; 1873 | } 1874 | 1875 | if (isset($safeUrlNameToAtt[$Element['name']])) 1876 | { 1877 | $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); 1878 | } 1879 | 1880 | if ( ! empty($Element['attributes'])) 1881 | { 1882 | foreach ($Element['attributes'] as $att => $val) 1883 | { 1884 | # filter out badly parsed attribute 1885 | if ( ! preg_match($goodAttribute, $att)) 1886 | { 1887 | unset($Element['attributes'][$att]); 1888 | } 1889 | # dump onevent attribute 1890 | elseif (self::striAtStart($att, 'on')) 1891 | { 1892 | unset($Element['attributes'][$att]); 1893 | } 1894 | } 1895 | } 1896 | 1897 | return $Element; 1898 | } 1899 | 1900 | protected function filterUnsafeUrlInAttribute(array $Element, $attribute) 1901 | { 1902 | foreach ($this->safeLinksWhitelist as $scheme) 1903 | { 1904 | if (self::striAtStart($Element['attributes'][$attribute], $scheme)) 1905 | { 1906 | return $Element; 1907 | } 1908 | } 1909 | 1910 | $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); 1911 | 1912 | return $Element; 1913 | } 1914 | 1915 | # 1916 | # Static Methods 1917 | # 1918 | 1919 | protected static function escape($text, $allowQuotes = false) 1920 | { 1921 | return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); 1922 | } 1923 | 1924 | protected static function striAtStart($string, $needle) 1925 | { 1926 | $len = strlen($needle); 1927 | 1928 | if ($len > strlen($string)) 1929 | { 1930 | return false; 1931 | } 1932 | else 1933 | { 1934 | return strtolower(substr($string, 0, $len)) === strtolower($needle); 1935 | } 1936 | } 1937 | 1938 | static function instance($name = 'default') 1939 | { 1940 | if (isset(self::$instances[$name])) 1941 | { 1942 | return self::$instances[$name]; 1943 | } 1944 | 1945 | $instance = new static(); 1946 | 1947 | self::$instances[$name] = $instance; 1948 | 1949 | return $instance; 1950 | } 1951 | 1952 | private static $instances = array(); 1953 | 1954 | # 1955 | # Fields 1956 | # 1957 | 1958 | protected $DefinitionData; 1959 | 1960 | # 1961 | # Read-Only 1962 | 1963 | protected $specialCharacters = array( 1964 | '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' 1965 | ); 1966 | 1967 | protected $StrongRegex = array( 1968 | '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', 1969 | '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', 1970 | ); 1971 | 1972 | protected $EmRegex = array( 1973 | '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', 1974 | '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', 1975 | ); 1976 | 1977 | protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; 1978 | 1979 | protected $voidElements = array( 1980 | 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 1981 | ); 1982 | 1983 | protected $textLevelElements = array( 1984 | 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', 1985 | 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', 1986 | 'i', 'rp', 'del', 'code', 'strike', 'marquee', 1987 | 'q', 'rt', 'ins', 'font', 'strong', 1988 | 's', 'tt', 'kbd', 'mark', 1989 | 'u', 'xm', 'sub', 'nobr', 1990 | 'sup', 'ruby', 1991 | 'var', 'span', 1992 | 'wbr', 'time', 1993 | ); 1994 | } 1995 | --------------------------------------------------------------------------------