diff options
author | Bradley Taunt <bt@btxx.org> | 2024-02-02 13:05:54 -0500 |
---|---|---|
committer | Bradley Taunt <bt@btxx.org> | 2024-02-02 13:05:54 -0500 |
commit | 13cec3d0fc257d0e65c9a1c06bfc71648722a506 (patch) | |
tree | aaf959aa898357abe14c45016a9071ce4d0587c0 |
29 files changed, 4909 insertions, 0 deletions
@@ -0,0 +1,8 @@ +ISC License: + +Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") +Copyright (c) 1995-2003 by Internet Software Consortium + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..69c8175 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# PHPetite + +PHPetite (/p/h/pəˈtēt/) is a single file, static blog generated from PHP. Based off the very minimal and awesome <a target="_blank" href="https://github.com/cadars/portable-php">portable-php</a>. + +## Key Features + +- Entire blog is rendered in a single HTML file +- Inline, compressed CSS +- All images converted into base64 encoding +- Minimal requirements / no heavy build tools + +## Core Principles + +The basic idea behind PHPetite is to keep the concept and workflow as simple as possible. Therefore, this project will try it's best to avoid bloat and feature creep. More elaborate and feature-rich blogging platforms should be used if your needs are not met with PHPetite. + +## Requirements + +1. `PHP 7.3` or higher +2. If using Linux, you will require the following packages in order to convert your images to base64 encoding: + - PHP XML -> `sudo apt-get install php-xml` + - PHP mbstring -> `sudo apt-get install php-mbstring` + +That's really it! + +## General Usage + +You can find most basic explanations and details on working with the project at [phpetite.btxx.org](https://phpetite.btxx.org) if you prefer. + +### Generating the Blog + +Get [PHPetite](https://git.btxx.org/phpetite "PHPetite") in order to convert a collection of Markdown files into a single HTML file with inline CSS. + +1. Make proper edits to the `/_phpetite/_config.php` file +2. Write posts in `/content` +3. (Optional) include any images under the `/content/img/` directory +4. From the command-line run: + +```.shell +make +``` + +This will generate both the single file HTML page, along with an `atom.xml` file for the use of an optional RSS feed. + +These two files are output into the `_site` directory. + +--- + +### Structuring Blog Posts + +Blog posts should be placed into the `/content` directory and be named based only on their post date. See an example here: + +```.markdown +2048-01-01.md +``` + +PHPetite will create a `target` by appending the page title inside the article to the file's date name. So a markdown file with the following content: + +```.markdown +# Bladerunner Rocks + +Bladerunner is amazing because blah blah blah... +``` + +will render out the `target` link as: + +```.markdown +example.com/#2048-01-01-bladerunner-rocks +``` + +--- + +### Adding Custom Pages + +To add your own custom pages, simply create a Markdown file under the `content/_pages` directory. PHPetite will take it from there! + +#### Some Cavets + +Any page you create will be automatically added to the `footer` navigation section. If you wish to hide individual pages from showing in the `footer`, do so via CSS: + +```.css +footer a.slug-name-of-your-page { + display: none; +} +``` + +If you want to remove the `footer` navigation altogether, add the following to your `style.css` file: + +```.css +footer .footer-links { + display: none; +} +``` + +--- + +## TODOs + +See the official, on-going feature list here: [https://phpetite.btxx.org/#about](https://phpetite.btxx.org/#about) + diff --git a/_phpetite/_config.php b/_phpetite/_config.php new file mode 100644 index 0000000..c4e29b6 --- /dev/null +++ b/_phpetite/_config.php @@ -0,0 +1,17 @@ +<?php + + // Your site title and meta description + $site_title = 'PHPetite'; + $site_desc = 'A single file, static blog generated from PHP'; + $site_url = 'https://phpetite.btxx.org'; + $site_author = 'Bradley Taunt'; + $site_email = 'bt@btxx.org'; + + // Activate or disable images to base64 strings + $images_to_base64 = true; + + // You probably don't need to change these :P + $site_style = 'style.css'; + $site_icon = 'content/img/icon.png'; + +?> diff --git a/_phpetite/dependencies/Parsedown.php b/_phpetite/dependencies/Parsedown.php new file mode 100644 index 0000000..ae0cbde --- /dev/null +++ b/_phpetite/dependencies/Parsedown.php @@ -0,0 +1,1994 @@ +<?php + +# +# +# Parsedown +# http://parsedown.org +# +# (c) Emanuil Rusev +# http://erusev.com +# +# For the full license information, view the LICENSE file that was distributed +# with this source code. +# +# + +class Parsedown +{ + # ~ + + const version = '1.8.0-beta-7'; + + # ~ + + function text($text) + { + $Elements = $this->textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + protected function textElements($text) + { + # make sure no definitions are set + $this->DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + return $this->linesElements($lines); + } + + # + # Setters + # + + function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + function setStrictMode($strictMode) + { + $this->strictMode = (bool) $strictMode; + + return $this; + } + + protected $strictMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'tel:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + return $this->elements($this->linesElements($lines)); + } + + protected function linesElements(array $lines) + { + $Elements = array(); + $CurrentBlock = null; + + foreach ($lines as $line) + { + if (chop($line) === '') + { + if (isset($CurrentBlock)) + { + $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) + ? $CurrentBlock['interrupted'] + 1 : 1 + ); + } + + continue; + } + + while (($beforeTab = strstr($line, "\t", true)) !== false) + { + $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; + + $line = $beforeTab + . str_repeat(' ', $shortage) + . substr($line, strlen($beforeTab) + 1) + ; + } + + $indent = strspn($line, ' '); + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; + $Block = $this->$methodName($Line, $CurrentBlock); + + if (isset($Block)) + { + $CurrentBlock = $Block; + + continue; + } + else + { + if ($this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{"block$blockType"}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') + { + $Block = $this->paragraphContinue($Line, $CurrentBlock); + } + + if (isset($Block)) + { + $CurrentBlock = $Block; + } + else + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + + # ~ + + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + # ~ + + return $Elements; + } + + protected function extractElement(array $Component) + { + if ( ! isset($Component['element'])) + { + if (isset($Component['markup'])) + { + $Component['element'] = array('rawHtml' => $Component['markup']); + } + elseif (isset($Component['hidden'])) + { + $Component['element'] = array(); + } + } + + return $Component['element']; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block' . $Type . 'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block' . $Type . 'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] >= 4) + { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) + { + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + $Block['element']['element']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['element']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (strpos($Line['text'], '<!--') === 0) + { + $Block = array( + 'element' => array( + 'rawHtml' => $Line['body'], + 'autobreak' => true, + ), + ); + + if (strpos($Line['text'], '-->') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + if (strpos($Line['text'], '-->') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + $marker = $Line['text'][0]; + + $openerLength = strspn($Line['text'], $marker); + + if ($openerLength < 3) + { + return; + } + + $infostring = trim(substr($Line['text'], $openerLength), "\t "); + + if (strpos($infostring, '`') !== false) + { + return; + } + + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if ($infostring !== '') + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); + + $Element['attributes'] = array('class' => "language-$language"); + } + + $Block = array( + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => array( + 'name' => 'pre', + 'element' => $Element, + ), + ); + + return $Block; + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] + and chop(substr($Line['text'], $len), ' ') === '' + ) { + $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['element']['text'] .= "\n" . $Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + $level = strspn($Line['text'], '#'); + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '#'); + + if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') + { + return; + } + + $text = trim($text, ' '); + + $Block = array( + 'element' => array( + 'name' => 'h' . $level, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + + # + # List + + protected function blockList($Line, array $CurrentBlock = null) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); + + if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) + { + $contentIndent = strlen($matches[2]); + + if ($contentIndent >= 5) + { + $contentIndent -= 1; + $matches[1] = substr($matches[1], 0, -$contentIndent); + $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; + } + elseif ($contentIndent === 0) + { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = strstr($matches[1], ' ', true); + + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'data' => array( + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), + ), + 'element' => array( + 'name' => $name, + 'elements' => array(), + ), + ); + $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); + + if ($name === 'ol') + { + $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; + + if ($listStart !== '1') + { + if ( + isset($CurrentBlock) + and $CurrentBlock['type'] === 'Paragraph' + and ! isset($CurrentBlock['interrupted']) + ) { + return; + } + + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) + { + return null; + } + + $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); + + if ($Line['indent'] < $requiredIndent + and ( + ( + $Block['data']['type'] === 'ol' + and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) or ( + $Block['data']['type'] === 'ul' + and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) + ) + ) { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['indent'] = $Line['indent']; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => array($text), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) + { + return null; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ($Line['indent'] >= $requiredIndent) + { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], $requiredIndent); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['elements'] as &$li) + { + if (end($li['handler']['argument']) !== '') + { + $li['handler']['argument'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => array( + 'function' => 'linesElements', + 'argument' => (array) $matches[1], + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block['element']['handler']['argument'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['handler']['argument'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + $marker = $Line['text'][0]; + + if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') + { + $Block = array( + 'element' => array( + 'name' => 'hr', + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed']) or isset($Block['interrupted'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (strpos($Line['text'], ']') !== false + and preg_match('/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + ) { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => isset($matches[3]) ? $matches[3] : null, + ); + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'element' => array(), + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ( + strpos($Block['element']['handler']['argument'], '|') === false + and strpos($Line['text'], '|') === false + and strpos($Line['text'], ':') === false + or strpos($Block['element']['handler']['argument'], "\n") !== false + ) { + return; + } + + if (chop($Line['text'], ' -:|') !== '') + { + return; + } + + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + return; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['handler']['argument']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + if (count($headerCells) !== count($alignments)) + { + return; + } + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ) + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => "text-align: $alignment;", + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'elements' => array(), + ), + ); + + $Block['element']['elements'] []= array( + 'name' => 'thead', + ); + + $Block['element']['elements'] []= array( + 'name' => 'tbody', + 'elements' => array(), + ); + + $Block['element']['elements'][0]['elements'] []= array( + 'name' => 'tr', + 'elements' => $HeaderElements, + ); + + return $Block; + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); + + $cells = array_slice($matches[0], 0, count($Block['alignments'])); + + foreach ($cells as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $cell, + 'destination' => 'elements', + ) + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'elements' => $Elements, + ); + + $Block['element']['elements'][1]['elements'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + return array( + 'type' => 'Paragraph', + 'element' => array( + 'name' => 'p', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $Line['text'], + 'destination' => 'elements', + ), + ), + ); + } + + protected function paragraphContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + $Block['element']['handler']['argument'] .= "\n".$Line['text']; + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!*_&[:<`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables = array()) + { + return $this->elements($this->lineElements($text, $nonNestables)); + } + + protected function lineElements($text, $nonNestables = array()) + { + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + $Elements = array(); + + $nonNestables = (empty($nonNestables) + ? array() + : array_combine($nonNestables, $nonNestables) + ); + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strlen($text) - strlen($excerpt); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + # check to see if the current inline type is nestable in the current context + + if (isset($nonNestables[$inlineType])) + { + continue; + } + + $Inline = $this->{"inline$inlineType"}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + + $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) + ? array_merge($Inline['element']['nonNestables'], $nonNestables) + : $nonNestables + ; + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + # compile the inline + $Elements[] = $this->extractElement($Inline); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + $text = substr($text, $markerPosition + 1); + } + + $InlineText = $this->inlineText($text); + $Elements[] = $InlineText['element']; + + foreach ($Elements as &$Element) + { + if ( ! isset($Element['autobreak'])) + { + $Element['autobreak'] = false; + } + } + + return $Elements; + } + + # + # ~ + # + + protected function inlineText($text) + { + $Inline = array( + 'extent' => strlen($text), + 'element' => array(), + ); + + $Inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + array( + array('name' => 'br'), + array('text' => "\n"), + ), + $text + ); + + return $Inline; + } + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(?<!['.$marker.'])\1(?!'.$marker.')/s', $Excerpt['text'], $matches)) + { + $text = $matches[2]; + $text = preg_replace('/[ ]*+\n/', ' ', $text); + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' + . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + + if (strpos($Excerpt['text'], '>') !== false + and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) + ){ + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = "mailto:$url"; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'element' => array('rawHtml' => $Excerpt['text'][1]), + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['handler']['argument'], + ), + 'autobreak' => true, + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => null, + 'destination' => 'elements', + ), + 'nonNestables' => array('Url', 'Link'), + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['handler']['argument'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['handler']['argument']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false + and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + ) { + return array( + 'element' => array('rawHtml' => '&' . $matches[1] . ';'), + 'extent' => strlen($matches[0]), + ); + } + + return; + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (strpos($Excerpt['context'], 'http') !== false + and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) + ) { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + $Inline = $this->inlineText($text); + return $this->element($Inline['element']); + } + + # + # Handlers + # + + protected function handle(array $Element) + { + if (isset($Element['handler'])) + { + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (is_string($Element['handler'])) + { + $function = $Element['handler']; + $argument = $Element['text']; + unset($Element['text']); + $destination = 'rawHtml'; + } + else + { + $function = $Element['handler']['function']; + $argument = $Element['handler']['argument']; + $destination = $Element['handler']['destination']; + } + + $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); + + if ($destination === 'handler') + { + $Element = $this->handle($Element); + } + + unset($Element['handler']); + } + + return $Element; + } + + protected function handleElementRecursive(array $Element) + { + return $this->elementApplyRecursive(array($this, 'handle'), $Element); + } + + protected function handleElementsRecursive(array $Elements) + { + return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); + } + + protected function elementApplyRecursive($closure, array $Element) + { + $Element = call_user_func($closure, $Element); + + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + } + + return $Element; + } + + protected function elementApplyRecursiveDepthFirst($closure, array $Element) + { + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); + } + + $Element = call_user_func($closure, $Element); + + return $Element; + } + + protected function elementsApplyRecursive($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursive($closure, $Element); + } + + return $Elements; + } + + protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); + } + + return $Elements; + } + + protected function element(array $Element) + { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + + # identity map if element has no handler + $Element = $this->handle($Element); + + $hasName = isset($Element['name']); + + $markup = ''; + + if ($hasName) + { + $markup .= '<' . $Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= " $name=\"".self::escape($value).'"'; + } + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + + if ($hasContent) + { + $markup .= $hasName ? '>' : ''; + + if (isset($Element['elements'])) + { + $markup .= $this->elements($Element['elements']); + } + elseif (isset($Element['element'])) + { + $markup .= $this->element($Element['element']); + } + else + { + if (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + } + + $markup .= $hasName ? '</' . $Element['name'] . '>' : ''; + } + elseif ($hasName) + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + $autoBreak = true; + + foreach ($Elements as $Element) + { + if (empty($Element)) + { + continue; + } + + $autoBreakNext = (isset($Element['autobreak']) + ? $Element['autobreak'] : isset($Element['name']) + ); + // (autobreak === false) covers both sides of an element + $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; + + $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $autoBreak = $autoBreakNext; + } + + $markup .= $autoBreak ? "\n" : ''; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $Elements = $this->linesElements($lines); + + if ( ! in_array('', $lines) + and isset($Elements[0]) and isset($Elements[0]['name']) + and $Elements[0]['name'] === 'p' + ) { + unset($Elements[0]['name']); + } + + return $Elements; + } + + # + # AST Convenience + # + + /** + * Replace occurrences $regexp with $Elements in $text. Return an array of + * elements representing the replacement. + */ + protected static function pregReplaceElements($regexp, $Elements, $text) + { + $newElements = array(); + + while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) + { + $offset = $matches[0][1]; + $before = substr($text, 0, $offset); + $after = substr($text, $offset + strlen($matches[0][0])); + + $newElements[] = array('text' => $before); + + foreach ($Elements as $Element) + { + $newElements[] = $Element; + } + + $text = $after; + } + + $newElements[] = array('text' => $text); + + return $newElements; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if ( ! isset($Element['name'])) + { + unset($Element['attributes']); + return $Element; + } + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/_phpetite/dependencies/ParsedownExtra.php b/_phpetite/dependencies/ParsedownExtra.php new file mode 100644 index 0000000..ed2ce73 --- /dev/null +++ b/_phpetite/dependencies/ParsedownExtra.php @@ -0,0 +1,685 @@ +<?php + +# +# +# Parsedown Extra +# https://github.com/erusev/parsedown-extra +# +# (c) Emanuil Rusev +# http://erusev.com +# +# For the full license information, view the LICENSE file that was distributed +# with this source code. +# +# + +class ParsedownExtra extends Parsedown +{ + # ~ + + const version = '0.8.0'; + + # ~ + + function __construct() + { + if (version_compare(parent::version, '1.7.1') < 0) + { + throw new Exception('ParsedownExtra requires a later version of Parsedown'); + } + + $this->BlockTypes[':'] []= 'DefinitionList'; + $this->BlockTypes['*'] []= 'Abbreviation'; + + # identify footnote definitions before reference definitions + array_unshift($this->BlockTypes['['], 'Footnote'); + + # identify footnote markers before before links + array_unshift($this->InlineTypes['['], 'FootnoteMarker'); + } + + # + # ~ + + function text($text) + { + $Elements = $this->textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + # merge consecutive dl elements + + $markup = preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup); + + # add footnotes + + if (isset($this->DefinitionData['Footnote'])) + { + $Element = $this->buildFootnoteElement(); + + $markup .= "\n" . $this->element($Element); + } + + return $markup; + } + + # + # Blocks + # + + # + # Abbreviation + + protected function blockAbbreviation($Line) + { + if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches)) + { + $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2]; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Footnote + + protected function blockFootnote($Line) + { + if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches)) + { + $Block = array( + 'label' => $matches[1], + 'text' => $matches[2], + 'hidden' => true, + ); + + return $Block; + } + } + + protected function blockFootnoteContinue($Line, $Block) + { + if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + if ($Line['indent'] >= 4) + { + $Block['text'] .= "\n\n" . $Line['text']; + + return $Block; + } + } + else + { + $Block['text'] .= "\n" . $Line['text']; + + return $Block; + } + } + + protected function blockFootnoteComplete($Block) + { + $this->DefinitionData['Footnote'][$Block['label']] = array( + 'text' => $Block['text'], + 'count' => null, + 'number' => null, + ); + + return $Block; + } + + # + # Definition List + + protected function blockDefinitionList($Line, $Block) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph') + { + return; + } + + $Element = array( + 'name' => 'dl', + 'elements' => array(), + ); + + $terms = explode("\n", $Block['element']['handler']['argument']); + + foreach ($terms as $term) + { + $Element['elements'] []= array( + 'name' => 'dt', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $term, + 'destination' => 'elements' + ), + ); + } + + $Block['element'] = $Element; + + $Block = $this->addDdElement($Line, $Block); + + return $Block; + } + + protected function blockDefinitionListContinue($Line, array $Block) + { + if ($Line['text'][0] === ':') + { + $Block = $this->addDdElement($Line, $Block); + + return $Block; + } + else + { + if (isset($Block['interrupted']) and $Line['indent'] === 0) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['dd']['handler']['function'] = 'textElements'; + $Block['dd']['handler']['argument'] .= "\n\n"; + + $Block['dd']['handler']['destination'] = 'elements'; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], min($Line['indent'], 4)); + + $Block['dd']['handler']['argument'] .= "\n" . $text; + + return $Block; + } + } + + # + # Header + + protected function blockHeader($Line) + { + $Block = parent::blockHeader($Line); + + if ($Block !== null && preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE)) + { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]); + } + + return $Block; + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + $length = strlen($matches[0]); + $remainder = substr($Line['text'], $length); + + if (trim($remainder) === '') + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + $Block['closed'] = true; + $Block['void'] = true; + } + } + else + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + return; + } + if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) + { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open + { + $Block['depth'] ++; + } + + if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close + { + if ($Block['depth'] > 0) + { + $Block['depth'] --; + } + else + { + $Block['closed'] = true; + } + } + + if (isset($Block['interrupted'])) + { + $Block['element']['rawHtml'] .= "\n"; + unset($Block['interrupted']); + } + + $Block['element']['rawHtml'] .= "\n".$Line['body']; + + return $Block; + } + + protected function blockMarkupComplete($Block) + { + if ( ! isset($Block['void'])) + { + $Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']); + } + + return $Block; + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + $Block = parent::blockSetextHeader($Line, $Block); + + if ($Block !== null && preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE)) + { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]); + } + + return $Block; + } + + # + # Inline Elements + # + + # + # Footnote Marker + + protected function inlineFootnoteMarker($Excerpt) + { + if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) + { + $name = $matches[1]; + + if ( ! isset($this->DefinitionData['Footnote'][$name])) + { + return; + } + + $this->DefinitionData['Footnote'][$name]['count'] ++; + + if ( ! isset($this->DefinitionData['Footnote'][$name]['number'])) + { + $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » & + } + + $Element = array( + 'name' => 'sup', + 'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name), + 'element' => array( + 'name' => 'a', + 'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'), + 'text' => $this->DefinitionData['Footnote'][$name]['number'], + ), + ); + + return array( + 'extent' => strlen($matches[0]), + 'element' => $Element, + ); + } + } + + private $footnoteCount = 0; + + # + # Link + + protected function inlineLink($Excerpt) + { + $Link = parent::inlineLink($Excerpt); + + $remainder = $Link !== null ? substr($Excerpt['text'], $Link['extent']) : ''; + + if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches)) + { + $Link['element']['attributes'] += $this->parseAttributeData($matches[1]); + + $Link['extent'] += strlen($matches[0]); + } + + return $Link; + } + + # + # ~ + # + + private $currentAbreviation; + private $currentMeaning; + + protected function insertAbreviation(array $Element) + { + if (isset($Element['text'])) + { + $Element['elements'] = self::pregReplaceElements( + '/\b'.preg_quote($this->currentAbreviation, '/').'\b/', + array( + array( + 'name' => 'abbr', + 'attributes' => array( + 'title' => $this->currentMeaning, + ), + 'text' => $this->currentAbreviation, + ) + ), + $Element['text'] + ); + + unset($Element['text']); + } + + return $Element; + } + + protected function inlineText($text) + { + $Inline = parent::inlineText($text); + + if (isset($this->DefinitionData['Abbreviation'])) + { + foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning) + { + $this->currentAbreviation = $abbreviation; + $this->currentMeaning = $meaning; + + $Inline['element'] = $this->elementApplyRecursiveDepthFirst( + array($this, 'insertAbreviation'), + $Inline['element'] + ); + } + } + + return $Inline; + } + + # + # Util Methods + # + + protected function addDdElement(array $Line, array $Block) + { + $text = substr($Line['text'], 1); + $text = trim($text); + + unset($Block['dd']); + + $Block['dd'] = array( + 'name' => 'dd', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements' + ), + ); + + if (isset($Block['interrupted'])) + { + $Block['dd']['handler']['function'] = 'textElements'; + + unset($Block['interrupted']); + } + + $Block['element']['elements'] []= & $Block['dd']; + + return $Block; + } + + protected function buildFootnoteElement() + { + $Element = array( + 'name' => 'div', + 'attributes' => array('class' => 'footnotes'), + 'elements' => array( + array('name' => 'hr'), + array( + 'name' => 'ol', + 'elements' => array(), + ), + ), + ); + + uasort($this->DefinitionData['Footnote'], array(__CLASS__, 'sortFootnotes')); + + foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData) + { + if ( ! isset($DefinitionData['number'])) + { + continue; + } + + $text = $DefinitionData['text']; + + $textElements = parent::textElements($text); + + $numbers = range(1, $DefinitionData['count']); + + $backLinkElements = array(); + + foreach ($numbers as $number) + { + $backLinkElements[] = array('text' => ' '); + $backLinkElements[] = array( + 'name' => 'a', + 'attributes' => array( + 'href' => "#fnref$number:$definitionId", + 'class' => 'footnote-backref', + ), + 'rawHtml' => '↩', + 'allowRawHtmlInSafeMode' => true, + 'autobreak' => false, + ); + } + + unset($backLinkElements[0]); + + $n = count($textElements) -1; + + if ($textElements[$n]['name'] === 'p') + { + $backLinkElements = array_merge( + array( + array( + 'rawHtml' => ' ', + 'allowRawHtmlInSafeMode' => true, + ), + ), + $backLinkElements + ); + + unset($textElements[$n]['name']); + + $textElements[$n] = array( + 'name' => 'p', + 'elements' => array_merge( + array($textElements[$n]), + $backLinkElements + ), + ); + } + else + { + $textElements[] = array( + 'name' => 'p', + 'elements' => $backLinkElements + ); + } + + $Element['elements'][1]['elements'] []= array( + 'name' => 'li', + 'attributes' => array('id' => 'fn:'.$definitionId), + 'elements' => array_merge( + $textElements + ), + ); + } + + return $Element; + } + + # ~ + + protected function parseAttributeData($attributeString) + { + $Data = array(); + + $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY); + + foreach ($attributes as $attribute) + { + if ($attribute[0] === '#') + { + $Data['id'] = substr($attribute, 1); + } + else # "." + { + $classes []= substr($attribute, 1); + } + } + + if (isset($classes)) + { + $Data['class'] = implode(' ', $classes); + } + + return $Data; + } + + # ~ + + protected function processTag($elementMarkup) # recursive + { + # http://stackoverflow.com/q/1148928/200145 + libxml_use_internal_errors(true); + + $DOMDocument = new DOMDocument; + + # http://stackoverflow.com/q/11309194/200145 + $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8'); + + # http://stackoverflow.com/q/4879946/200145 + $DOMDocument->loadHTML($elementMarkup); + $DOMDocument->removeChild($DOMDocument->doctype); + $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild); + + $elementText = ''; + + if ($DOMDocument->documentElement->getAttribute('markdown') === '1') + { + foreach ($DOMDocument->documentElement->childNodes as $Node) + { + $elementText .= $DOMDocument->saveHTML($Node); + } + + $DOMDocument->documentElement->removeAttribute('markdown'); + + $elementText = "\n".$this->text($elementText)."\n"; + } + else + { + foreach ($DOMDocument->documentElement->childNodes as $Node) + { + $nodeMarkup = $DOMDocument->saveHTML($Node); + + if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements)) + { + $elementText .= $this->processTag($nodeMarkup); + } + else + { + $elementText .= $nodeMarkup; + } + } + } + + # because we don't want for markup to get encoded + $DOMDocument->documentElement->nodeValue = 'placeholder\x1A'; + + $markup = $DOMDocument->saveHTML($DOMDocument->documentElement); + $markup = str_replace('placeholder\x1A', $elementText, $markup); + + return $markup; + } + + # ~ + + protected function sortFootnotes($A, $B) # callback + { + return $A['number'] - $B['number']; + } + + # + # Fields + # + + protected $regexAttribute = '(?:[#.][-\w]+[ ]*)'; +} diff --git a/_phpetite/dependencies/ParsedownExtraPlugin.php b/_phpetite/dependencies/ParsedownExtraPlugin.php new file mode 100755 index 0000000..55e9091 --- /dev/null +++ b/_phpetite/dependencies/ParsedownExtraPlugin.php @@ -0,0 +1,585 @@ +<?php + +# +# +# Parsedown Extra Plugin +# https://github.com/tovic/parsedown-extra-plugin +# +# (c) Emanuil Rusev +# http://erusev.com +# +# (c) Taufik Nurrohman +# https://mecha-cms.com +# +# For the full license information, view the LICENSE file that was distributed +# with this source code. +# +# + +class ParsedownExtraPlugin extends ParsedownExtra { + + const version = '1.3.1'; + + + # config + + public $abbreviationData = array(); + + public $blockCodeAttributes = array(); + + public $blockCodeClassFormat = 'language-%s'; + + public $blockCodeHtml = null; + + public $blockQuoteAttributes = array(); + + public $blockQuoteText = null; + + public $codeAttributes = array(); + + public $codeAttributesOnParent = false; + + public $codeHtml = null; + + public $figureAttributes = array(); + + public $figuresEnabled = false; + + public $footnoteAttributes = array(); + + public $footnoteBackLinkAttributes = array(); + + public $footnoteBackLinkHtml = null; + + public $footnoteBackReferenceAttributes = array(); + + public $footnoteLinkAttributes = array(); + + public $footnoteLinkHtml = null; + + public $footnoteReferenceAttributes = array(); + + public $headerAttributes = array(); + + public $headerText = null; + + public $imageAttributes = array(); + + public $imageAttributesOnParent = false; + + public $linkAttributes = array(); + + public $referenceData = array(); + + public $tableAttributes = array(); + + public $tableColumnAttributes = array(); + + public $voidElementSuffix = ' />'; + + # config + + + protected $regexAttribute = '(?:[#.][-\w:\\\]+[ ]*|[-\w:\\\]+(?:=(?:["\'][^\n]*?["\']|[^\s]+)?)?[ ]*)'; + + # Method aliases for every configuration property + public function __call($key, array $arguments = array()) { + $property = lcfirst(substr($key, 3)); + if (strpos($key, 'set') === 0 && property_exists($this, $property)) { + $this->{$property} = $arguments[0]; + return $this; + } + throw new Exception('Method ' . $key . ' does not exists.'); + } + + public function __construct() { + if (version_compare(parent::version, '0.8.0-beta-1') < 0) { + throw new Exception('ParsedownExtraPlugin requires a later version of Parsedown'); + } + $this->BlockTypes['!'][] = 'Image'; + parent::__construct(); + } + + protected function blockAbbreviation($Line) { + // Allow empty abbreviations + if (preg_match('/^\*\[(.+?)\]:[ ]*$/', $Line['text'], $matches)) { + $this->DefinitionData['Abbreviation'][$matches[1]] = null; + return array('hidden' => true); + } + return parent::blockAbbreviation($Line); + } + + protected function blockCodeComplete($Block) { + $this->doSetAttributes($Block['element']['element'], $this->blockCodeAttributes); + $this->doSetContent($Block['element']['element'], $this->blockCodeHtml, true); + // Put code attributes on parent element + if ($this->codeAttributesOnParent) { + if ($this->codeAttributesOnParent === true) { + // $this->codeAttributesOnParent = array_keys($Block['element']['element']['attributes']); + $this->codeAttributesOnParent = array('class', 'id'); + } + foreach ((array) $this->codeAttributesOnParent as $Name) { + if (isset($Block['element']['element']['attributes'][$Name])) { + $Block['element']['attributes'][$Name] = $Block['element']['element']['attributes'][$Name]; + unset($Block['element']['element']['attributes'][$Name]); + } + } + } + $Block['element']['element']['rawHtml'] = $Block['element']['element']['text']; + $Block['element']['element']['allowRawHtmlInSafeMode'] = true; + unset($Block['element']['element']['text']); + return $Block; + } + + protected function blockFencedCode($Line) { + // Re-enable the multiple class name feature + $Line['text'] = strtr(trim($Line['text']), array( + ' ' => "\x1A", + '.' => "\x1A." + )); + // Enable custom attribute syntax on code block + $Attributes = array(); + if (strpos($Line['text'], '{') !== false && substr($Line['text'], -1) === '}') { + $Parts = explode('{', $Line['text'], 2); + $Attributes = $this->parseAttributeData(strtr(substr($Parts[1], 0, -1), "\x1A", ' ')); + $Line['text'] = trim($Parts[0]); + } + if (!$Block = parent::blockFencedCode($Line)) { + return; + } + if ($Attributes) { + $Block['element']['element']['attributes'] = $Attributes; + } else if (isset($Block['element']['element']['attributes']['class'])) { + $Classes = explode("\x1A", strtr($Block['element']['element']['attributes']['class'], ' ', "\x1A")); + // `~~~ php` → `<pre><code class="language-php">` + // `~~~ php html` → `<pre><code class="language-php language-html">` + // `~~~ .php` → `<pre><code class="php">` + // `~~~ .php.html` → `<pre><code class="php html">` + // `~~~ .php html` → `<pre><code class="php language-html">` + // `~~~ {.php #foo}` → `<pre><code id="foo" class="php">` + $Results = []; + foreach ($Classes as $Class) { + if ($Class === "" || $Class === str_replace('%s', "", $this->blockCodeClassFormat)) { + continue; + } + if ($Class[0] === '.') { + $Results[] = substr($Class, 1); + } else { + $Results[] = sprintf($this->blockCodeClassFormat, $Class); + } + } + $Block['element']['element']['attributes']['class'] = implode(' ', array_unique($Results)); + } + return $Block; + } + + protected function blockFencedCodeComplete($Block) { + return $this->blockCodeComplete($Block); + } + + protected function blockHeader($Line) { + if (!$Block = parent::blockHeader($Line)) { + return; + } + $Level = strspn($Line['text'], '#'); + $this->doSetAttributes($Block['element'], $this->headerAttributes, array($Level)); + $this->doSetContent($Block['element'], $this->headerText, false, 'argument', array($Level)); + return $Block; + } + + protected function blockImage($Line) { + if (!$this->figuresEnabled) { + return; + } + // Match exactly an image syntax in a paragraph (with optional custom attributes, and optional hard break marker) + if (preg_match('/^\!\[[^\n]*?\](\[[^\n]*?\]|\([^\n]*?\))(\s*\{' . $this->regexAttribute . '+?\})?([ ]{2})?$/', $Line['text'])) { + $Block = array( + 'description' => "", + 'element' => array( + 'name' => 'figure', + 'attributes' => array(), + 'elements' => array( + $this->inlineImage($Line) + ) + ) + ); + $this->doSetAttributes($Block['element'], $this->figureAttributes); + return $Block; + } + return; + } + + protected function blockImageComplete($Block) { + if (!empty($Block['description'])) { + $Description = $Block['description']; + $Block['element']['elements'][] = array( + 'name' => 'figcaption', + 'rawHtml' => $this->{strpos($Description, "\n\n") === false ? 'line' : 'text'}(trim($Description, "\n")) + ); + // unset($Block['description']); + } + if ($this->imageAttributesOnParent) { + $Inline = $Block['element']['elements'][0]; + if ($this->imageAttributesOnParent === true) { + $this->imageAttributesOnParent = array_keys($Inline['element']['attributes']); + } + foreach ((array) $this->imageAttributesOnParent as $Name) { + if (isset($Inline['element']['attributes'][$Name])) { + // Merge class names + if ( + $Name === 'class' && + isset($Block['element']['attributes'][$Name]) && + isset($Inline['element']['attributes'][$Name]) + ) { + $Classes = array_merge( + explode(' ', $Block['element']['attributes'][$Name]), + explode(' ', $Inline['element']['attributes'][$Name]) + ); + sort($Classes); + $Block['element']['attributes']['class'] = implode(' ', array_unique(array_filter($Classes))); + unset($Block['element']['elements'][0]['element']['attributes'][$Name]); + continue; + } + $Block['element']['attributes'][$Name] = $Inline['element']['attributes'][$Name]; + unset($Block['element']['elements'][0]['element']['attributes'][$Name]); + } + } + } + return $Block; + } + + protected function blockImageContinue($Line, array $Block) { + if (isset($Block['complete'])) { + return; + } + if (isset($Block['interrupted'])) { + $Block['description'] .= "\n"; + unset($Block['interrupted']); + } + if ($Line['indent'] === 0) { + $Block['complete'] = true; + return; + } + if ($Line['indent'] > 0 && $Line['indent'] < 4) { + $Block['description'] .= "\n" . $Line['text']; + return $Block; + } + return; + } + + protected function blockQuoteComplete($Block) { + $this->doSetAttributes($Block['element'], $this->blockQuoteAttributes); + $this->doSetContent($Block['element'], $this->blockQuoteText, false, 'arguments'); + return $Block; + } + + protected function blockSetextHeader($Line, array $Block = null) { + if (!$Block = parent::blockSetextHeader($Line, $Block)) { + return; + } + $Level = $Line['text'][0] === '=' ? 1 : 2; + $this->doSetAttributes($Block['element'], $this->headerAttributes, array($Level)); + $this->doSetContent($Block['element'], $this->headerText, false, 'argument', array($Level)); + return $Block; + } + + protected function blockTableContinue($Line, array $Block) { + if (!$Block = parent::blockTableContinue($Line, $Block)) { + return; + } + $Aligns = $Block['alignments']; + // `<thead>` or `<tbody>` + foreach ($Block['element']['elements'] as $Index0 => &$Element0) { + // `<tr>` + foreach ($Element0['elements'] as $Index1 => &$Element1) { + // `<th>` or `<td>` + foreach ($Element1['elements'] as $Index2 => &$Element2) { + $this->doSetAttributes($Element2, $this->tableColumnAttributes, array($Aligns[$Index2], $Index2, $Index1)); + } + } + } + return $Block; + } + + protected function blockTableComplete($Block) { + $this->doSetAttributes($Block['element'], $this->tableAttributes); + return $Block; + } + + protected function buildFootnoteElement() { + $DefinitionData = $this->DefinitionData['Footnote']; + if (!$Footnotes = parent::buildFootnoteElement()) { + return; + } + $DefinitionKey = array_keys($DefinitionData); + $DefinitionData = array_values($DefinitionData); + $this->doSetAttributes($Footnotes, $this->footnoteAttributes); + foreach ($Footnotes['elements'][1]['elements'] as $Index0 => &$Element0) { + $Name = $DefinitionKey[$Index0]; + $Count = $DefinitionData[$Index0]['count']; + $Args = array(is_numeric($Name) ? (float) $Name : $Name, $Count); + $this->doSetAttributes($Element0, $this->footnoteBackReferenceAttributes, $Args); + foreach ($Element0['elements'] as $Index1 => &$Element1) { + $Count = 0; + foreach ($Element1['elements'] as $Index2 => &$Element2) { + if (!isset($Element2['name']) || $Element2['name'] !== 'a') { + continue; + } + $Args[1] = ++$Count; + $this->doSetAttributes($Element2, $this->footnoteBackLinkAttributes, $Args); + $this->doSetContent($Element2, $this->footnoteBackLinkHtml, false, 'rawHtml'); + } + } + } + return $Footnotes; + } + + protected function doGetAttributes($Element) { + if (isset($Element['attributes'])) { + return (array) $Element['attributes']; + } + return array(); + } + + protected function doGetContent($Element) { + if (isset($Element['text'])) { + return $Element['text']; + } + if (isset($Element['rawHtml'])) { + return $Element['rawHtml']; + } + if (isset($Element['handler']['argument'])) { + return implode("\n", (array) $Element['handler']['argument']); + } + return null; + } + + private function doSetLink($Excerpt, $Function) { + if (!$Inline = parent::$Function($Excerpt)) { + return; + } + $this->doSetAttributes($Inline['element'], $this->linkAttributes, array($this->isLocal($Inline['element'], 'href'))); + $this->doSetData($this->DefinitionData['Reference'], $this->referenceData); + return $Inline; + } + + protected function doSetAttributes(&$Element, $From, $Args = array()) { + $Attributes = $this->doGetAttributes($Element); + $Content = $this->doGetContent($Element); + if (is_callable($From)) { + $Args = array_merge(array($Content, $Attributes, &$Element), $Args); + $Element['attributes'] = array_replace($Attributes, (array) call_user_func_array($From, $Args)); + } else { + $Element['attributes'] = array_replace($Attributes, (array) $From); + } + } + + protected function doSetContent(&$Element, $From, $Esc = false, $Mode = 'text', $Args = array()) { + $Attributes = $this->doGetAttributes($Element); + $Content = $this->doGetContent($Element); + if ($Esc) { + $Content = parent::escape($Content, true); + } + if (is_callable($From)) { + $Args = array_merge(array($Content, $Attributes, &$Element), $Args); + $Content = call_user_func_array($From, $Args); + } else if (!empty($From)) { + $Content = sprintf($From, $Content); + } + if ($Mode === 'arguments') { + $Element['handler']['argument'] = explode("\n", $Content); + } else if ($Mode === 'argument') { + $Element['handler']['argument'] = $Content; + } else { + $Element[$Mode] = $Content; + } + } + + protected function doSetData(&$To, $From) { + $To = array_replace((array) $To, (array) $From); + } + + protected function element(array $Element) { + if (!$Any = parent::element($Element)) { + return; + } + if (substr($Any, -3) === ' />') { + if (is_callable($this->voidElementSuffix)) { + $Attributes = $this->doGetAttributes($Element); + $Content = $this->doGetContent($Element); + $Suffix = call_user_func_array($this->voidElementSuffix, [$Content, $Attributes, &$Element]); + } else { + $Suffix = $this->voidElementSuffix; + } + $Any = substr_replace($Any, $Suffix, -3); + } + return $Any; + } + + protected function inlineCode($Excerpt) { + if (!$Inline = parent::inlineCode($Excerpt)) { + return; + } + $this->doSetAttributes($Inline['element'], $this->codeAttributes); + $this->doSetContent($Inline['element'], $this->codeHtml, true); + $Inline['element']['rawHtml'] = $Inline['element']['text']; + $Inline['element']['allowRawHtmlInSafeMode'] = true; + unset($Inline['element']['text']); + return $Inline; + } + + protected function inlineFootnoteMarker($Excerpt) { + if (!$Inline = parent::inlineFootnoteMarker($Excerpt)) { + return; + } + $Name = null; + if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) { + $Name = $matches[1]; + } + $Args = array(is_numeric($Name) ? (float) $Name : $Name, $this->DefinitionData['Footnote'][$Name]['count']); + $this->doSetAttributes($Inline['element'], $this->footnoteReferenceAttributes, $Args); + $this->doSetAttributes($Inline['element']['element'], $this->footnoteLinkAttributes, $Args); + $this->doSetContent($Inline['element']['element'], $this->footnoteLinkHtml, false, 'text', $Args); + $Inline['element']['element']['rawHtml'] = $Inline['element']['element']['text']; + $Inline['element']['element']['allowRawHtmlInSafeMode'] = true; + unset($Inline['element']['element']['text']); + return $Inline; + } + + protected function inlineImage($Excerpt) { + if (!$Inline = parent::inlineImage($Excerpt)) { + return; + } + $this->doSetAttributes($Inline['element'], $this->imageAttributes, array($this->isLocal($Inline['element'], 'src'))); + return $Inline; + } + + protected function inlineLink($Excerpt) { + return $this->doSetLink($Excerpt, __FUNCTION__); + } + + protected function inlineText($Text) { + $this->doSetData($this->DefinitionData['Abbreviation'], $this->abbreviationData); + return parent::inlineText($Text); + } + + protected function inlineUrl($Excerpt) { + return $this->doSetLink($Excerpt, __FUNCTION__); + } + + protected function inlineUrlTag($Excerpt) { + return $this->doSetLink($Excerpt, __FUNCTION__); + } + + protected function isLocal($Element, $Key) { + $Link = isset($Element['attributes'][$Key]) ? (string) $Element['attributes'][$Key] : null; + if ( + // `<a href="">` + $Link === "" || + // `<a href="../foo/bar">` + // `<a href="/foo/bar">` + // `<a href="?foo=bar">` + // `<a href="&foo=bar">` + // `<a href="#foo">` + strpos('./?&#', $Link[0]) !== false && strpos($Link, '//') !== 0 || + // `<a href="data:text/html,asdf">` + strpos($Link, 'data:') === 0 || + // `<a href="javascript:;">` + strpos($Link, 'javascript:') === 0 || + // `<a href="mailto:as@df">` + strpos($Link, 'mailto:') === 0 + ) { + return true; + } + if (isset($_SERVER['HTTP_HOST'])) { + $Host = $_SERVER['HTTP_HOST']; + } else if (isset($_SERVER['SERVER_NAME'])) { + $Host = $_SERVER['SERVER_NAME']; + } else { + $Host = ""; + } + // `<a href="//example.com">` + if (strpos($Link, '//') === 0 && strpos($Link, '//' . $Host) !== 0) { + return false; + } + if ( + // `<a href="https://127.0.0.1">` + strpos($Link, 'https://' . $Host) === 0 || + // `<a href="http://127.0.0.1">` + strpos($Link, 'http://' . $Host) === 0 + ) { + return true; + } + // `<a href="foo/bar">` + return strpos($Link, '://') === false; + } + + protected function parseAttributeData($attributeString) { + // Allow compact attributes + $attributeString = strtr($attributeString, array( + '#' => ' #', + '.' => ' .' + )); + if (strpos($attributeString, '="') !== false || strpos($attributeString, "='") !== false) { + $attributeString = preg_replace_callback('#([-\w]+=)(["\'])([^\n]*?)\2#', function($matches) { + $value = strtr($matches[3], array( + ' #' => '#', + ' .' => '.', + ' ' => "\x1A" + )); + return $matches[1] . $matches[2] . $value . $matches[2]; + }, $attributeString); + } + $Attributes = array(); + foreach (explode(' ', $attributeString) as $v) { + if (!$v) { + continue; + } + // `{#foo}` + if ($v[0] === '#' && isset($v[1])) { + $Attributes['id'] = substr($v, 1); + // `{.foo}` + } else if ($v[0] === '.' && isset($v[1])) { + $Attributes['class'][] = substr($v, 1); + // ~ + } else if (strpos($v, '=') !== false) { + $vv = explode('=', $v, 2); + // `{foo=}` + if ($vv[1] === "") { + if ($vv[0] === 'class') { + continue; + } + $Attributes[$vv[0]] = ""; + // `{foo="bar baz"}` + // `{foo='bar baz'}` + } else if ($vv[1][0] === '"' && substr($vv[1], -1) === '"' || $vv[1][0] === "'" && substr($vv[1], -1) === "'") { + $values = stripslashes(strtr(substr(substr($vv[1], 1), 0, -1), "\x1A", ' ')); + if ($vv[0] === 'class' && isset($Attributes[$vv[0]])) { + $values = explode(' ', $values); + $Attributes[$vv[0]] = array_merge($Attributes[$vv[0]], $values); + } else { + $Attributes[$vv[0]] = $values; + } + // `{foo=bar}` + } else { + if ($vv[0] === 'class' && isset($Attributes[$vv[0]])) { + $Attributes[$vv[0]] = array_merge($Attributes[$vv[0]], [$vv[1]]); + } else { + $Attributes[$vv[0]] = $vv[1]; + } + } + // `{foo}` + } else { + if ($v === 'class' && isset($Attributes[$v])) { + continue; + } + $Attributes[$v] = $v; + } + } + if (isset($Attributes['class'])) { + $Attributes['class'] = implode(' ', array_unique((array) $Attributes['class'])); + } + return $Attributes; + } + +} diff --git a/_phpetite/phpetite.php b/_phpetite/phpetite.php new file mode 100644 index 0000000..9090480 --- /dev/null +++ b/_phpetite/phpetite.php @@ -0,0 +1,109 @@ +<?php +// PHPetite v.1.0 (Based off the excellent work of Portable PHP) +// Render each of the Markdown files from a folder in a <section>, with date-and-title as #id. + +// !!! +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// DO NOT TOUCH anything below here unless you know what you're doing. +// Most basic use cases won't need to change anything here. +// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// !!! + +// Include custom site configurations +include($_SERVER['DOCUMENT_ROOT'].'_phpetite/_config.php'); +include($_SERVER['DOCUMENT_ROOT'].'_phpetite/system.php'); + + +// $site_info takes all page content from /content/_pages/home-content.md +$site_info = '<div class="site-info">' . $parsedown->text(file_get_contents('content/_pages/home-content.md')) .'</div>'; + +$cssFiles = array( + "style.css" +); + +$updated_date = date("F j, Y"); +$base_64_favicon = base64_encode(file_get_contents($site_icon)); + +$buffer = ""; +foreach ($cssFiles as $cssFile) { + $buffer .= file_get_contents($cssFile); +} +$buffer = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $buffer); +$buffer = str_replace(': ', ':', $buffer); +$buffer = str_replace(array("\r\n", "\r", "\n", "\t", ' ', ' ', ' '), '', $buffer); + +// Decide whether or not to display the site info on homepage +if ($site_info != '') { + $show_site_info = $site_info . '<hr>'; +} else { + $show_site_info = ''; +} + +$html = <<<EOD +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <title>$site_title</title> + <meta name="description" content="$site_desc"> + <meta name="color-scheme" content="dark light"> + <link rel="icon" href="data:image/png;base64,$base_64_favicon"> + <!-- og tags --> + <meta property="og:title" content="$site_title"> + <meta property="og:description" content="$site_desc"> + <!-- other --> + <meta name="twitter:card" content="summary"> + <style>$buffer</style> +</head> +<body> + <header> + <h1> + <a href="#home">$site_title</a> + </h1> + </header> + <main> + $posts + $pages + <section tabindex="0" role="document" aria-label="Home" id="home"> + $show_site_info + <nav> + <ul class="toc"> + $toc + </ul> + </nav> + </section> + </main> + <footer> + <small>Last updated on $updated_date</small> + <div class="footer-links"> + <small>$pages_footer</small> + </div> + </footer> +</body> +</html> +EOD; + +if ($images_to_base64 == true) { + $dom = new DOMDocument(); + libxml_use_internal_errors(true); + $dom->loadHTML(html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8')); + libxml_clear_errors(); + $post_images = $dom->getElementsByTagName('img'); + foreach ($post_images as $image) { + $src = $image->getAttribute('src'); + $type = pathinfo($src, PATHINFO_EXTENSION); + $data = file_get_contents($src); + $base64 = 'data:image/' . $type . ';base64,' . base64_encode($data); + $image->setAttribute("src", $base64); + } + + $html = $dom->saveHTML(); + echo $html; +} else { + echo $html; +} + +?>
\ No newline at end of file diff --git a/_phpetite/rss.php b/_phpetite/rss.php new file mode 100644 index 0000000..9acd8bc --- /dev/null +++ b/_phpetite/rss.php @@ -0,0 +1,17 @@ +<?php + // Include custom site configurations +include ($_SERVER['DOCUMENT_ROOT'].'_phpetite/_config.php'); + include ($_SERVER['DOCUMENT_ROOT'].'_phpetite/system.php'); + + echo '<?xml version="1.0" encoding="utf-8"?> + <feed xmlns="http://www.w3.org/2005/Atom"> + <title>'.$site_title.'</title> + <link href="'.$site_url.'/atom.xml" rel="self"/> + <link href="'.$site_url.'"/> + <updated>'.date("Y-m-d\TH:i:sP").'</updated> + <id>'.$site_url.'/</id> + <author> + <name>'.$site_author.'</name> + <email>'.$site_email.'</email> + </author>'.$rss_items.'</feed>'; +?>
\ No newline at end of file diff --git a/_phpetite/system.php b/_phpetite/system.php new file mode 100644 index 0000000..10f43f8 --- /dev/null +++ b/_phpetite/system.php @@ -0,0 +1,117 @@ +<?php + +// !!! +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// DO NOT TOUCH anything below here unless you know what you're doing. +// Most basic use cases won't need to change anything here. +// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// !!! + +// Dependencies +include('dependencies/Parsedown.php'); +include('dependencies/ParsedownExtra.php'); +include('dependencies/ParsedownExtraPlugin.php'); + +// Set variable blank defaults +$toc = ''; +$posts = ''; +$pages = ''; +$pages_footer = ''; +$rss_items = ''; +$site_info = ''; + +function create_slug($string){ + $string = strtolower($string); + $string = strip_tags($string); + $string = stripslashes($string); + $string = html_entity_decode($string); + $string = str_replace('\'', '', $string); + $string = trim(preg_replace('/[^a-z0-9]+/', '-', $string), '-'); + return $string; +} + +$files = []; +foreach (new DirectoryIterator(__DIR__.'/../content/') as $file) { + if ( $file->getType() == 'file' && strpos($file->getFilename(),'.md') ) { + $files[] = $file->getFilename(); + } +} +rsort($files); + +foreach ($files as $file) { + + $filename_no_ext = substr($file, 0, strrpos($file, ".")); + $file_path = __DIR__.'/../content/'.$file; + $file = fopen($file_path, 'r'); + $post_title = trim(fgets($file),'#'); + $post_slug = create_slug($filename_no_ext.$post_title); + fclose($file); + + $parsedown = new ParsedownExtraPlugin(); + // Allow single line breaks + $parsedown->setBreaksEnabled(true); + // Add image dimensions, lazy loading and figures + $parsedown->imageAttributes = ['width', 'height']; + $parsedown->imageAttributes = ['loading' => 'lazy']; + $parsedown->figuresEnabled = true; + // Remove the id and #links on footnotes + $parsedown->footnoteLinkAttributes = function() {return ['href' => '#'];}; + $parsedown->footnoteReferenceAttributes = function() {return ['id' => null];}; + $parsedown->footnoteBackLinkAttributes = function() {return ['href' => '#'];}; + $parsedown->footnoteBackReferenceAttributes = function() {return ['id' => null];}; + + $toc .= '<li><a href="#'.$post_slug.'"><span>'.$post_title.'</span></a> <time datetime="'.substr($filename_no_ext, 0, 10).'">'.substr($filename_no_ext, 0, 10).'</time></li>'; + $posts .= '<section tabindex="0" role="document" aria-label="'.$post_title.'" id="'.$post_slug.'"><time class="posted-on" datetime="'.substr($filename_no_ext, 0, 10).'">'.substr($filename_no_ext, 0, 10).'</time>'.$parsedown->text(file_get_contents($file_path)).'<hr></section>'; + + $rss_items .= ' + <entry> + <title>'.trim($post_title, " \t\n\r").'</title> + <link href="'.$site_url.'#'.$post_slug.'"/> + <updated>'.substr($filename_no_ext, 0, 10).'T00:00:00+00:00</updated> + <id>'.$site_url.'/#'.$post_slug.'</id> + <content type="html">'.htmlspecialchars($parsedown->text(file_get_contents($file_path)), ENT_XML1, 'UTF-8').'</content> + </entry> + '; + +} + +$files_pages = []; +foreach (new DirectoryIterator(__DIR__.'/../content/_pages/') as $file_page) { + if ( $file_page->getType() == 'file' && strpos($file_page->getFilename(),'.md') ) { + $files_pages[] = $file_page->getFilename(); + } +} +rsort($files_pages); + +foreach ($files_pages as $file_page) { + + $filename_no_ext_page = substr($file_page, 0, strrpos($file_page, ".")); + $file_path_page = __DIR__.'/../content/_pages/'.$file_page; + $file_page = fopen($file_path_page, 'r'); + $page_title = trim(fgets($file_page),'# '); + $page_slug = create_slug($filename_no_ext_page); + fclose($file_page); + + $parsedown = new ParsedownExtraPlugin(); + // Allow single line breaks + $parsedown->setBreaksEnabled(true); + // Add image dimensions, lazy loading and figures + $parsedown->imageAttributes = ['width', 'height']; + $parsedown->imageAttributes = ['loading' => 'lazy']; + $parsedown->figuresEnabled = true; + // Remove the id and #links on footnotes + $parsedown->footnoteLinkAttributes = function() {return ['href' => '#'];}; + $parsedown->footnoteReferenceAttributes = function() {return ['id' => null];}; + $parsedown->footnoteBackLinkAttributes = function() {return ['href' => '#'];}; + $parsedown->footnoteBackReferenceAttributes = function() {return ['id' => null];}; + + if ($page_slug != 'home-content') { + $pages .= '<section tabindex="0" role="document" aria-label="'.$page_title.'" id="'.$page_slug.'">'.$parsedown->text(file_get_contents($file_path_page)).'</section>'; + } + $pages_footer .='<a class="'.$page_slug.'" href="#'.$page_slug.'">'.trim($page_title, " \t\n\r").'</a><span class="divider">/</span>'; + +} + +?>
\ No newline at end of file diff --git a/_site/atom.xml b/_site/atom.xml new file mode 100644 index 0000000..9bc9ffe --- /dev/null +++ b/_site/atom.xml @@ -0,0 +1,183 @@ +<?xml version="1.0" encoding="utf-8"?> + <feed xmlns="http://www.w3.org/2005/Atom"> + <title>PHPetite</title> + <link href="https://phpetite.btxx.org/atom.xml" rel="self"/> + <link href="https://phpetite.btxx.org"/> + <updated>2024-02-02T18:05:24+00:00</updated> + <id>https://phpetite.btxx.org/</id> + <author> + <name>Bradley Taunt</name> + <email>bt@btxx.org</email> + </author> + <entry> + <title>Cleaning Things Up & Future PHPetite Updates</title> + <link href="https://phpetite.btxx.org#2022-06-28-cleaning-things-up-future-phpetite-updates"/> + <updated>2022-06-28T00:00:00+00:00</updated> + <id>https://phpetite.btxx.org/#2022-06-28-cleaning-things-up-future-phpetite-updates</id> + <content type="html"><h1>Cleaning Things Up &amp; Future PHPetite Updates</h1> +<p>It has been quite a long time since I've reviewed or updated this little project of mine. Since it's release, I've created another minimal blogging system (based on <code>bash</code> this time) called <a href="https://shinobi.website">Shinobi</a> and converted my personal website to use <em>that</em> instead.</p> +<p>But I still love this single file blogging concept. So, I thought it was time for some basic cleanup. That "cleanup" slowly turned into a TODO list of sorts and now there are extra features I plan to add.</p> +<h2>The Cleanup</h2> +<p>* I first started by including the specific <code>$_SERVER['DOCUMENT_ROOT'].</code> parameter in the main PHP includes. I noticed when pulling the project in a "fresh" instance that the build failed without this setup. Sorry for anyone who may have ran into this issue previously!</p> +<p>* The default build script has been moved into a proper <code>makefile</code>. Now, generating the website only requires you to run <code>make</code> from the main directory. Running <code>make serve</code> builds the website and also runs a local server for testing at <code>localhost:8000</code>. Nothing groundbreaking but pretty helpful.</p> +<p>* The original dark mode CSS styling has been removed in favor of using the browser supported <code>color scheme</code> meta tag.</p> +<p>* Post dates are now listed at the top of each blog article (see above for reference)</p> +<h2>What's to Come</h2> +<p>I keep a running list of features I plan to implement (in no particular order) on the main <a href="#about">about section</a>. Feel free to open an issue on the <a href="https://todo.sr.ht/~bt/phpetite">official sourcehut todo</a> if you have other features and suggestions. Don't be shy!</p> +<h2>Follow Along</h2> +<p>Rolling out any new updates for this project will take time. I'm in no <em>real</em> rush and I do have other projects that require my attention. That said, if you wish to stay up-to-date, I recommend following along via the <a href="/atom.xml">official RSS feed</a>.</p> +<p>Thanks for reading and happy single-file blogging!</p> +<p>-- Brad</p></content> + </entry> + + <entry> + <title>Converting Custom Fonts to Base64 Strings</title> + <link href="https://phpetite.btxx.org#2021-02-27-converting-custom-fonts-to-base64-strings"/> + <updated>2021-02-27T00:00:00+00:00</updated> + <id>https://phpetite.btxx.org/#2021-02-27-converting-custom-fonts-to-base64-strings</id> + <content type="html"><h1>Converting Custom Fonts to Base64 Strings</h1> +<p>There are currently no plans to automatically convert custom fonts to base64 strings within the project itself - <strong>but</strong> it is very easy to do so manually for Mac/Linux users.</p> +<p>Simply open a terminal window and navigate to where your custom font file is located. The enter the following command (replacing the font extension name with your appropriate file name):</p> +<pre><code class="bash">base64 your-custom-font.woff2 &gt; font-base64.txt</code></pre> +<p>Then in your <code>style.css</code> file, add the custom font as you normally would via the <code>@font-face</code> property but this time utilizing base64:</p> +<pre><code class="css">@font-face { + font-family: 'FontName; + src: url(data:font/woff2;base64,[BASE64 CODE]) format('woff2'); +}</code></pre> +<h2>Things to Keep in Mind</h2> +<p>Remember that by using base64 strings you are <em>significantly</em> increasing the overall size of your single file project. This should be used for extreme use cases where a single file website/blog isn't allowed access to 3rd party URLs or extra files on the root server. Hence why by default it isn't include in the PHPetite project itself.</p> +<h2>Live Example</h2> +<p>You can check out the <a href="https://thrifty.name">ThriftyName</a> project (built on PHPetite) to see base64 custom fonts in use.</p></content> + </entry> + + <entry> + <title>Disable Image to Base64 Conversion</title> + <link href="https://phpetite.btxx.org#2021-02-18-disable-image-to-base64-conversion"/> + <updated>2021-02-18T00:00:00+00:00</updated> + <id>https://phpetite.btxx.org/#2021-02-18-disable-image-to-base64-conversion</id> + <content type="html"><h1>Disable Image to Base64 Conversion</h1> +<p>Some users<sup><a href="#" class="footnote-ref">2</a></sup> may wish to host their imagery and media via a 3rd party source or simply want to avoid the heavy weight added with using base64 strings (~133%+ in size). </p> +<p>To disable this feature, open your <code>_phpetite/_config.php</code> file and change the <code>images_to_base64</code> variable to false.</p> +<pre><code class="php">// Activate or disable images to base64 strings +$images_to_base64 = false;</code></pre> +<div class="footnotes"> +<hr /> +<ol> +<li> +<p>Thanks to <a href="https://news.ycombinator.com/user?id=Minor49er">Minor49er</a> for suggesting this option on <a href="https://news.ycombinator.com/item?id=26175904">Hacker News</a>&#160;<a href="#" class="footnote-backref">&#8617;</a></p> +</li> +</ol> +</div></content> + </entry> + + <entry> + <title>Automatic RSS</title> + <link href="https://phpetite.btxx.org#2021-02-08-automatic-rss"/> + <updated>2021-02-08T00:00:00+00:00</updated> + <id>https://phpetite.btxx.org/#2021-02-08-automatic-rss</id> + <content type="html"><h1>Automatic RSS</h1> +<p>PHPetite ships with a very basic and crude auto-generated RSS feed. When you run the project's build script:</p> +<pre><code class="bash">bash build.sh</code></pre> +<p>it not only generates the single HTML blog file, but also creates an <code>atom.xml</code> file in the root directory. Simply share this with your followers or link it somewhere on your site itself (eg. <code>yourdomain.com/atom.xml</code>).</p> +<p>You can view this site's RSS feed here: </p> +<p><a href="https://phpetite.org/atom.xml">https://phpetite.org/atom.xml</a></p></content> + </entry> + + <entry> + <title>The Benefits of a Single File Blog</title> + <link href="https://phpetite.btxx.org#2021-02-07-the-benefits-of-a-single-file-blog"/> + <updated>2021-02-07T00:00:00+00:00</updated> + <id>https://phpetite.btxx.org/#2021-02-07-the-benefits-of-a-single-file-blog</id> + <content type="html"><h1>The Benefits of a Single File Blog</h1> +<p>Rendering your blog or website as a single file using PHPetite is pretty fantastic. It gives you the freedom to easily share, host or edit your site's content on almost any hosting provider. </p> +<p>Since the entire blog's content is generated inline on <code>build</code>, you don't need to fiddle around with external <code>CSS</code> and <code>JS</code> files. There is also no need to worry about broken <code>img</code> sources since PHPetite converts all images into proper base64 strings.</p> +<p>Using this website as an example: this blog weighs in at <strong>~21KB</strong><sup><a href="#" class="footnote-ref">2</a></sup>. </p> +<p>That is incredibly tiny in terms of website size. Some sections on other web pages are larger than that!</p> +<p>The portability of having a <em>single</em> <code>HTML</code> file as your blog is quite liberating. Though it should be noted, if your blog consists of high resolutions imagery or includes massive amounts of content, a single file might be a little impractical for you.</p> +<h2>Hosting for Newcomers</h2> +<p>I suggest using <a href="https://app.netlify.com/drop">Netlify Drop</a> if this is your first time setting up a hosting environment or you don't consider yourself too tech-savvy. Once you have your rendered <code>index.html</code> file, simply drag-and-drop the file into Netlify Drop - that's it!</p> +<p>From there you can always setup a permanent subdomain or use your own custom domain.</p> +<h2>Local Development</h2> +<p>Simply follow the instructions found on the <a href="#generating-this-blog">Generating This Blog</a> page and you'll be running a local version of your site in seconds.</p> +<div class="footnotes"> +<hr /> +<ol> +<li> +<p>At this time of writing (Feb 2021)&#160;<a href="#" class="footnote-backref">&#8617;</a></p> +</li> +</ol> +</div></content> + </entry> + + <entry> + <title>Converting from Jekyll</title> + <link href="https://phpetite.btxx.org#2021-02-06-converting-from-jekyll"/> + <updated>2021-02-06T00:00:00+00:00</updated> + <id>https://phpetite.btxx.org/#2021-02-06-converting-from-jekyll</id> + <content type="html"><h1>Converting from Jekyll</h1> +<p>This walkthrough is still being tweaked and optimized. Check back soon for the final version!</p></content> + </entry> + + <entry> + <title>Markdown examples</title> + <link href="https://phpetite.btxx.org#2021-01-09-markdown-examples"/> + <updated>2021-01-09T00:00:00+00:00</updated> + <id>https://phpetite.btxx.org/#2021-01-09-markdown-examples</id> + <content type="html"><h1><abbr title="Markdown is a lightweight markup language for creating formatted text using a plain-text editor">Markdown</abbr> examples</h1> +<p>On top of plain <abbr title="Markdown is a lightweight markup language for creating formatted text using a plain-text editor">Markdown</abbr>, <a href="https://michelf.ca/projects/php-markdown/extra"><abbr title="Markdown is a lightweight markup language for creating formatted text using a plain-text editor">Markdown</abbr> Extra</a> adds support for footnotes, abbreviations, definition lists, tables, <code>class</code> and <code>id</code> attributes, fenced code blocks, and <abbr title="Markdown is a lightweight markup language for creating formatted text using a plain-text editor">Markdown</abbr> inside <abbr title="Hypertext Markup Language">HTML</abbr> blocks.</p> +<p>Additionally, images are properly enclosed in figure elements (with optional figcaption), and the <code>loading="lazy"</code> attribute is added.</p> +<hr /> +<p>This is <strong>bold</strong>, <em>italic</em>, this is an <a href="#2021-01-11-hello-world">internal link</a>, this is <del>not</del> <code>code</code>, press <kbd>alt</kbd>.</p> +<figure><img src="content/img/image.png" alt="This is the image alt text" title="This is the image title." width="1280" height="800" loading="lazy" /><figcaption>This is the image caption (line begins with a space). The image above is actually a rendered base64 encoding.</figcaption> +</figure> +<blockquote> +<p>This text is in a blockquote.</p> +</blockquote> +<h2>This is a level 2 heading</h2> +<h3>This is a level 3 heading</h3> +<ul> +<li>This</li> +<li>is</li> +<li>a list</li> +</ul> +<ol start="0"> +<li>This</li> +<li>is</li> +<li>an</li> +<li>ordered list</li> +</ol> +<pre><code class="txt">This is + preformatted + text.</code></pre> +<table> +<thead> +<tr> +<th>this is a table</th> +<th>header</th> +<th style="text-align: right;">this column is right-aligned</th> +</tr> +</thead> +<tbody> +<tr> +<td>these</td> +<td>content</td> +<td style="text-align: right;">1234</td> +</tr> +<tr> +<td>are</td> +<td>cells</td> +<td style="text-align: right;">56789</td> +</tr> +</tbody> +</table> +<p>This sentence has a footnote.<sup><a href="#" class="footnote-ref">2</a></sup></p> +<div class="footnotes"> +<hr /> +<ol> +<li> +<p>This is a footnote&#160;<a href="#" class="footnote-backref">&#8617;</a></p> +</li> +</ol> +</div></content> + </entry> + </feed>
\ No newline at end of file diff --git a/_site/index.html b/_site/index.html new file mode 100644 index 0000000..c286c76 --- /dev/null +++ b/_site/index.html @@ -0,0 +1,251 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <title>PHPetite</title> + <meta name="description" content="A single file, static blog generated from PHP"> + <meta name="color-scheme" content="dark light"> + <link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAIElEQVR42mNgGAWDDfwnEo4aMGrAqAGjBowEA0bBwAAARraOgF2Dq4IAAAAASUVORK5CYII="> + <!-- og tags --> + <meta property="og:title" content="PHPetite"> + <meta property="og:description" content="A single file, static blog generated from PHP"> + <!-- other --> + <meta name="twitter:card" content="summary"> + <style>* {margin:0;padding:0;box-sizing:border-box;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-kerning:auto;}body {font:16px / 1.44 Lucida Grande, system-ui, "Segoe UI", sans-serif;position:relative;max-width:45em; margin:0 auto;}pre {background:rgba(255,255,255,0.1);border:1px solid;}section, section:target ~ section:last-of-type {height:0;overflow:hidden;padding:0;}section:target, section:last-of-type {height:auto;overflow:visible;padding:calc(5vw + 2.6em) 4vw 1.6em;}section:focus {outline:0;}section {overflow-wrap:break-word;width:100%;}section .posted-on {display:block;font-size:80%;margin-bottom:-0.8em;padding-top:1em;}section * + * {margin-top:.7em;}header {padding:5vw 4vw 0 4vw;position:absolute;width:100%;z-index:1;}footer {display:flex;flex-wrap:wrap;justify-content:space-between;padding:1em 4vw 5vw 4vw;}footer .footer-links {display:block;width:100%;}footer .footer-links a {display:inline-block;}footer .footer-links span.divider {display:inline-block;margin:0 5px;}footer .footer-links span.divider:last-of-type {display:none;}footer .footer-links a.home-content, footer .footer-links a.home-content + span.divider {display:none;}footer * + * {margin-top:0;}ul.toc {overflow:hidden;}ul.toc * + * {margin:0;}ul.toc li {line-height:1.5;position:relative;display:flex;align-items:flex-end;margin:0;}ul.toc li a {flex:1;}ul.toc li a span {padding-right:.3em;}ul.toc li time {font-variant-numeric:tabular-nums;padding-left:.3em;z-index:1;}ul.toc li:after {width:100%;font-size:.55em;position:absolute;bottom:.3em;white-space:nowrap;opacity:0.3;pointer-events:none;content:' . . . . . . . . . . . . . . . . . . . . . .'' . . . . . . . . . . . . . . . . . . . . . .'' . . . . . . . . . . . . . . . . . . . . . .'' . . . . . . . . . . . . . . . . . . . . . .'' . . . . . . . . . . . . . . . . . . . . . .'' . . . . . . . . . . . . . . . . . . . . . .';}a {text-decoration:none;overflow-wrap:break-word;}@media (hover:hover) and (pointer:fine) {a:hover {text-decoration:underline;}}a[href*="//"]:after {font-size:.65em;content:"\2197";display:inline-block;}a[href*="//"]:hover:after {color:inherit;}header h1 {margin-bottom:1em;}header h1 a {font-weight:normal;display:block;}section h1 {font-size:1.4em;line-height:1.5;padding-top:0;}header h1, h2, h3, h4, strong, b, dt {font-size:1em;}* + h2, * + h3, * + h4 {margin-top:1.4em;}h2.cute, h3 {text-transform:uppercase;letter-spacing:.06em;font-size:.9em;font-weight:400;}li, dd {margin-left:1.5em;}li + li, li ol, li ul {margin-top:.1em;}.footnotes li {margin-top:.5em;max-width:95%;}img {display:block;max-width:100%;min-height:6em;height:auto;position:relative;}img:after { align-content:center;border:1px dashed;content:attr(alt);display:grid;font-size:.8em;height:100%;left:0;position:absolute;text-align:center;top:0;width:100%;z-index:2;}figure {padding:1em;}figcaption, small, .footnotes {font-size:.865em;}blockquote {padding:0 1em;}cite {font-style:normal;}abbr[title] {text-decoration:none;cursor:help;}a abbr[title] {cursor:pointer;color:inherit;}hr {border:0;height:0;border-bottom:1px solid;opacity:.1;margin:1.4em 0;}sup {line-height:1;font-size:.75em;margin-left:.05em;}code, kbd {font-family:ui-monospace, SF Mono, SFMono-Regular, Menlo, Andale Mono, monospace;font-size:.9em;overflow-wrap:break-word;}kbd {box-shadow:0 .5px 1px;border-radius:2px;padding:.05em .325em;font-size:.85em;margin:0 .1em;}pre {line-height:1.55;overflow:auto;background:rgba(0,0,0,.03);padding:.5em .85em .6em .85em;border-radius:4px;}pre code {font-size:.9em;position:relative;display:block;overflow-wrap:normal;}pre code:after {content:attr(class);position:absolute;right:-.6em;top:-.3em;text-transform:uppercase;font-size:.7em;opacity:.45;}input, select, textarea, button {font:inherit;font-size:.85em;line-height:inherit;border:0;box-shadow:0 0 .05em;padding:.2em .4em;width:100%;}table {border-collapse:collapse;min-width:100%;margin:1em 0;}thead {text-align:left;border-bottom:1px solid;}tr + tr {border-top:1px solid;}th, td {padding:.4em .3em .2em;}sup a {color:currentColor;pointer-events:none;}a.footnote-backref {display:none;}@media print {header {position:relative;}section {height:auto;overflow:visible;page-break-after:always;page-break-inside:avoid;break-inside:avoid;display:block;padding:2em 4vw;}section * {page-break-inside:avoid;break-inside:avoid;}}@media only screen and (max-width:500px) {footer .footer-links a { display:block; margin:0.2em 0; }footer .footer-links span.divider { display:none; }blockquote, figure { padding-left:4vw; padding-right:4vw; }ul.toc li {align-items:flex-start;flex-direction:column-reverse;}ul.toc li a { padding-bottom:1em; }ul.toc li time { font-size:.8em; padding-left:0; }ul.toc li a:after {height:0;overflow:hidden;position:absolute;}}@supports (color-scheme:dark light) {@media screen and (prefers-color-scheme:dark) {a:link {color:#9e9eff;}a:visited {color:#d0adf0;}a:active {color:red;}}}</style> +</head> +<body> + <header> + <h1> + <a href="#home">PHPetite</a> + </h1> + </header> + <main> + <section tabindex="0" role="document" aria-label=" Cleaning Things Up & Future PHPetite Updates +" id="2022-06-28-cleaning-things-up-future-phpetite-updates"><time class="posted-on" datetime="2022-06-28">2022-06-28</time><h1>Cleaning Things Up & Future PHPetite Updates</h1> +<p>It has been quite a long time since I've reviewed or updated this little project of mine. Since it's release, I've created another minimal blogging system (based on <code>bash</code> this time) called <a href="https://shinobi.website">Shinobi</a> and converted my personal website to use <em>that</em> instead.</p> +<p>But I still love this single file blogging concept. So, I thought it was time for some basic cleanup. That "cleanup" slowly turned into a TODO list of sorts and now there are extra features I plan to add.</p> +<h2>The Cleanup</h2> +<p>* I first started by including the specific <code>$_SERVER['DOCUMENT_ROOT'].</code> parameter in the main PHP includes. I noticed when pulling the project in a "fresh" instance that the build failed without this setup. Sorry for anyone who may have ran into this issue previously!</p> +<p>* The default build script has been moved into a proper <code>makefile</code>. Now, generating the website only requires you to run <code>make</code> from the main directory. Running <code>make serve</code> builds the website and also runs a local server for testing at <code>localhost:8000</code>. Nothing groundbreaking but pretty helpful.</p> +<p>* The original dark mode CSS styling has been removed in favor of using the browser supported <code>color scheme</code> meta tag.</p> +<p>* Post dates are now listed at the top of each blog article (see above for reference)</p> +<h2>What's to Come</h2> +<p>I keep a running list of features I plan to implement (in no particular order) on the main <a href="#about">about section</a>. Feel free to open an issue on the <a href="https://todo.sr.ht/~bt/phpetite">official sourcehut todo</a> if you have other features and suggestions. Don't be shy!</p> +<h2>Follow Along</h2> +<p>Rolling out any new updates for this project will take time. I'm in no <em>real</em> rush and I do have other projects that require my attention. That said, if you wish to stay up-to-date, I recommend following along via the <a href="/atom.xml">official RSS feed</a>.</p> +<p>Thanks for reading and happy single-file blogging!</p> +<p>-- Brad</p><hr></section><section tabindex="0" role="document" aria-label=" Converting Custom Fonts to Base64 Strings +" id="2021-02-27-converting-custom-fonts-to-base64-strings"><time class="posted-on" datetime="2021-02-27">2021-02-27</time><h1>Converting Custom Fonts to Base64 Strings</h1> +<p>There are currently no plans to automatically convert custom fonts to base64 strings within the project itself - <strong>but</strong> it is very easy to do so manually for Mac/Linux users.</p> +<p>Simply open a terminal window and navigate to where your custom font file is located. The enter the following command (replacing the font extension name with your appropriate file name):</p> +<pre><code class="bash">base64 your-custom-font.woff2 > font-base64.txt</code></pre> +<p>Then in your <code>style.css</code> file, add the custom font as you normally would via the <code>@font-face</code> property but this time utilizing base64:</p> +<pre><code class="css">@font-face { + font-family: 'FontName; + src: url(data:font/woff2;base64,[BASE64 CODE]) format('woff2'); +}</code></pre> +<h2>Things to Keep in Mind</h2> +<p>Remember that by using base64 strings you are <em>significantly</em> increasing the overall size of your single file project. This should be used for extreme use cases where a single file website/blog isn't allowed access to 3rd party URLs or extra files on the root server. Hence why by default it isn't include in the PHPetite project itself.</p> +<h2>Live Example</h2> +<p>You can check out the <a href="https://thrifty.name">ThriftyName</a> project (built on PHPetite) to see base64 custom fonts in use.</p><hr></section><section tabindex="0" role="document" aria-label=" Disable Image to Base64 Conversion +" id="2021-02-18-disable-image-to-base64-conversion"><time class="posted-on" datetime="2021-02-18">2021-02-18</time><h1>Disable Image to Base64 Conversion</h1> +<p>Some users<sup><a href="#" class="footnote-ref">1</a></sup> may wish to host their imagery and media via a 3rd party source or simply want to avoid the heavy weight added with using base64 strings (~133%+ in size). </p> +<p>To disable this feature, open your <code>_phpetite/_config.php</code> file and change the <code>images_to_base64</code> variable to false.</p> +<pre><code class="php">// Activate or disable images to base64 strings +$images_to_base64 = false;</code></pre> +<div class="footnotes"> +<hr> +<ol> +<li> +<p>Thanks to <a href="https://news.ycombinator.com/user?id=Minor49er">Minor49er</a> for suggesting this option on <a href="https://news.ycombinator.com/item?id=26175904">Hacker News</a> <a href="#" class="footnote-backref">↩</a></p> +</li> +</ol> +</div><hr></section><section tabindex="0" role="document" aria-label=" Automatic RSS +" id="2021-02-08-automatic-rss"><time class="posted-on" datetime="2021-02-08">2021-02-08</time><h1>Automatic RSS</h1> +<p>PHPetite ships with a very basic and crude auto-generated RSS feed. When you run the project's build script:</p> +<pre><code class="bash">bash build.sh</code></pre> +<p>it not only generates the single HTML blog file, but also creates an <code>atom.xml</code> file in the root directory. Simply share this with your followers or link it somewhere on your site itself (eg. <code>yourdomain.com/atom.xml</code>).</p> +<p>You can view this site's RSS feed here: </p> +<p><a href="https://phpetite.org/atom.xml">https://phpetite.org/atom.xml</a></p><hr></section><section tabindex="0" role="document" aria-label=" The Benefits of a Single File Blog +" id="2021-02-07-the-benefits-of-a-single-file-blog"><time class="posted-on" datetime="2021-02-07">2021-02-07</time><h1>The Benefits of a Single File Blog</h1> +<p>Rendering your blog or website as a single file using PHPetite is pretty fantastic. It gives you the freedom to easily share, host or edit your site's content on almost any hosting provider. </p> +<p>Since the entire blog's content is generated inline on <code>build</code>, you don't need to fiddle around with external <code>CSS</code> and <code>JS</code> files. There is also no need to worry about broken <code>img</code> sources since PHPetite converts all images into proper base64 strings.</p> +<p>Using this website as an example: this blog weighs in at <strong>~21KB</strong><sup><a href="#" class="footnote-ref">1</a></sup>. </p> +<p>That is incredibly tiny in terms of website size. Some sections on other web pages are larger than that!</p> +<p>The portability of having a <em>single</em> <code>HTML</code> file as your blog is quite liberating. Though it should be noted, if your blog consists of high resolutions imagery or includes massive amounts of content, a single file might be a little impractical for you.</p> +<h2>Hosting for Newcomers</h2> +<p>I suggest using <a href="https://app.netlify.com/drop">Netlify Drop</a> if this is your first time setting up a hosting environment or you don't consider yourself too tech-savvy. Once you have your rendered <code>index.html</code> file, simply drag-and-drop the file into Netlify Drop - that's it!</p> +<p>From there you can always setup a permanent subdomain or use your own custom domain.</p> +<h2>Local Development</h2> +<p>Simply follow the instructions found on the <a href="#generating-this-blog">Generating This Blog</a> page and you'll be running a local version of your site in seconds.</p> +<div class="footnotes"> +<hr> +<ol> +<li> +<p>At this time of writing (Feb 2021) <a href="#" class="footnote-backref">↩</a></p> +</li> +</ol> +</div><hr></section><section tabindex="0" role="document" aria-label=" Converting from Jekyll +" id="2021-02-06-converting-from-jekyll"><time class="posted-on" datetime="2021-02-06">2021-02-06</time><h1>Converting from Jekyll</h1> +<p>This walkthrough is still being tweaked and optimized. Check back soon for the final version!</p><hr></section><section tabindex="0" role="document" aria-label=" Markdown examples +" id="2021-01-09-markdown-examples"><time class="posted-on" datetime="2021-01-09">2021-01-09</time><h1><abbr title="Markdown is a lightweight markup language for creating formatted text using a plain-text editor">Markdown</abbr> examples</h1> +<p>On top of plain <abbr title="Markdown is a lightweight markup language for creating formatted text using a plain-text editor">Markdown</abbr>, <a href="https://michelf.ca/projects/php-markdown/extra"><abbr title="Markdown is a lightweight markup language for creating formatted text using a plain-text editor">Markdown</abbr> Extra</a> adds support for footnotes, abbreviations, definition lists, tables, <code>class</code> and <code>id</code> attributes, fenced code blocks, and <abbr title="Markdown is a lightweight markup language for creating formatted text using a plain-text editor">Markdown</abbr> inside <abbr title="Hypertext Markup Language">HTML</abbr> blocks.</p> +<p>Additionally, images are properly enclosed in figure elements (with optional figcaption), and the <code>loading="lazy"</code> attribute is added.</p> +<hr> +<p>This is <strong>bold</strong>, <em>italic</em>, this is an <a href="#2021-01-11-hello-world">internal link</a>, this is <del>not</del> <code>code</code>, press <kbd>alt</kbd>.</p> +<figure><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABQAAAAMgBAMAAACOttLIAAAAMFBMVEXx8fHr6+vGxsbZ2dnAwMDf39+FhYWysrK5ubnl5eWNjY2lpaXNzc2VlZWdnZ3T09MWD5k/AAAGdElEQVR4Ae3YA3Ss6R3H8V+0u7msbbvp2sxatZGzyfnf7eQm6z2o7Ta13aZrO4vaOqqNqW133juzdyfLuTU+n9GL53mOvsE8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACA6wIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwA1ul+W2vn8GMbJ34G81Urun6/jDs8x4nZhBvKq1ENgk1TPdF+DibJYZrukM4i41Efi7A7zL+iwz9JmvZxC3nr9eNglMTk4eXJ23/fsCHFvIcjfMQIYmsslgZaXRBAgC5P82wLX77rmQ5OX3SzJ2p2N/lp6t7t/c3++ZE7nc2v2e+dNm0DZ52X57pWdkm85rh2x97E+TXx77njR++dxD0+hcvX1ePpE0t+6eq0KAe7SrDkuyz3Qyfl5V693pqjOSkc9Uzd0/XWs/0727cna0qr6Xrs1mO6+ZW1bNXu+xVa2m0D9V1Snp6Fyo3feZ6l06M1eBAF9cS1W36wb4uvripTXTF+Cv6weX1HS6PlgnX1TrmwDvMndJzV2vP8DzTv5Mnbl4crtOTFZVa2mx3p2MLdYXFw9vAhyuuUtrfiFXhgBPul1uVG/tBnjeYckja+GKABffnnxwLhuM1+nJXVrJytZpC9mldu8LcG4mI4tzh2W8va7Zmv5phustya3rwox85jNTybatidyyWfBKEOB0kvPWNwF2m1tbt7s8wO5XlC3q/mmsXZpoTiaysr7WTPxaX4DNuG2bt1fNJX84O8mzp5O7zKapcCr5zDuTnDcduFKATXP59UwvwCPT6Avw3VmuSXJl/TRJe6o/wOv13tZUuradSdqPT7KqpjLWtJlXrQtc3TbMTdZ1A1ycXh7gWH3+agO8XpLz+gOcbVacb976Ahzq/s2tqYzW/ZOsngtcXYBNQE2A+9S3JvoDzHl11v1zhQfttu+GAHO1Ac5uXHZsq18eP5ORempv4MraqmPXClxbgFtU1VnX6wvwllWti9Ozql0dAwT4yeqY6a7dC7ArcG0B5jXtqtP7Asx2n6n6djYYaledfOkAAd6mqrXUXh7g0gaBaw0wQ7/9TE30BZjxDy22rpfG5nVZMjJAgMfP/jTZtgnwyHS0mwADgwTYdHZkX4Adt6l3p3GTuQwU4FB9uvcteMPBUE11d3JgkACb8Po/u0N6SWX4ugPsjr/LTG/jb7Smsra5dB0Q4KqlnyZDfQFusbSQjfU8tLutfJ0BjjULDH1mJtm2tZB8sKaSxROTvP6cZPwjgWsIcKzemdy03r0xwOH6evKmWkhjdb07Y+3rDjCfWZ88tmaak3dstd1ceyq5y/z9O3Onk0e27h+4+gBzfJ2z7+JsNgY41G4de2CTUmO45p/bPmKAAO9S331x/Xqm+7253tkM3LyZW0cm+/hjfO3sA9ZRy/cBq7V7uvapar17gABHq+qUzZpsRz9T6xaagc3cekeSX9dPA8mqyTTGJyeaUA5Ktt4/ycuWlu6ensl3J3nw0vnvSc/4gZ2bkxO9qe/vddnMbV5ZddDly75i6ayF0b3SMb5Vr9Txey498XpJ1j4l8C/Vfkv+HeBjZycZrzPy7wDbzl8veVO9O/8OsLLeNblvrcu/BQw9uzqemn8PGHtBnfy8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwH+qv+ASmOBBNn44AAAAASUVORK5CYII=" alt="This is the image alt text" title="This is the image title." width="1280" height="800" loading="lazy"><figcaption>This is the image caption (line begins with a space). The image above is actually a rendered base64 encoding.</figcaption> +</figure> +<blockquote> +<p>This text is in a blockquote.</p> +</blockquote> +<h2>This is a level 2 heading</h2> +<h3>This is a level 3 heading</h3> +<ul> +<li>This</li> +<li>is</li> +<li>a list</li> +</ul> +<ol start="0"> +<li>This</li> +<li>is</li> +<li>an</li> +<li>ordered list</li> +</ol> +<pre><code class="txt">This is + preformatted + text.</code></pre> +<table> +<thead> +<tr> +<th>this is a table</th> +<th>header</th> +<th style="text-align: right;">this column is right-aligned</th> +</tr> +</thead> +<tbody> +<tr> +<td>these</td> +<td>content</td> +<td style="text-align: right;">1234</td> +</tr> +<tr> +<td>are</td> +<td>cells</td> +<td style="text-align: right;">56789</td> +</tr> +</tbody> +</table> +<p>This sentence has a footnote.<sup><a href="#" class="footnote-ref">1</a></sup></p> +<div class="footnotes"> +<hr> +<ol> +<li> +<p>This is a footnote <a href="#" class="footnote-backref">↩</a></p> +</li> +</ol> +</div><hr></section> + <section tabindex="0" role="document" aria-label="Structuring Blog Posts +" id="structure"><h1>Structuring Blog Posts</h1> +<p>Blog posts should be placed into the <code>/content</code> directory and be named based only on their post date. See an example here:</p> +<pre><code class="markdown">2048-01-01.md</code></pre> +<p>PHPetite will create a <code>target</code> by appending the page title inside the article to the file's date name. So a markdown file with the following content:</p> +<pre><code class="markdown"># Bladerunner Rocks + +Bladerunner is amazing because blah blah blah...</code></pre> +<p>will render out the <code>target</code> link as:</p> +<pre><code class="markdown">example.com/#2048-01-01-bladerunner-rocks</code></pre></section><section tabindex="0" role="document" aria-label="Requirements +" id="requirements"><h1>Requirements</h1> +<ol> +<li><code>PHP 7.3</code> or higher</li> +<li>If using Linux, you will require the following packages in order to convert your images to base64 encoding:<ul> +<li>PHP XML -> <code>sudo apt-get install php-xml</code></li> +<li>PHP mbstring -> <code>sudo apt-get install php-mbstring</code></li> +</ul> +</li> +</ol> +<p>That's really it!</p></section><section tabindex="0" role="document" aria-label="Generating This Blog +" id="generating-this-blog"><h1>Generating This Blog</h1> +<p><strong>Important</strong>: Before building and uploading your single file blog, be sure to edit all the proper details found inside the <code>_phpetite/_config.php</code> file. This includes your domain, site title, author name, etc. </p> +<p>Most users won't ever need to fiddle with the other files inside the <code>_phpetite</code> directory.</p> +<hr> +<p>Get <a href="https://git.btxx.org/phpetite" title="PHPetite">PHPetite</a> in order to convert a collection of Markdown files into a single <abbr title="Hyper Text Markup Language">HTML</abbr> file with inline <abbr title="Cascading Style Sheets">CSS</abbr>.</p> +<ol> +<li>Make proper edits to the <code>/_phpetite/_config.php</code> file</li> +<li>Write posts in <code>/content</code></li> +<li>(Optional) include any images under the <code>/content/img/</code> directory</li> +<li>From the command-line run:</li> +</ol> +<pre><code class="shell">make</code></pre> +<p>This will generate both the single file <abbr title="Hyper Text Markup Language">HTML</abbr> page, along with an <code>atom.xml</code> file for the use of an optional RSS feed.</p> +<p>These two files are output into the <code>_site</code> directory.</p> +<h2>Looking for more advanced options?</h2> +<ul> +<li><a href="#adding-pages">Adding Custom Pages</a></li> +<li><a href="#2021-02-07-converting-from-jekyll">Converting from Jekyll</a></li> +</ul></section><section tabindex="0" role="document" aria-label="Adding Custom Pages +" id="adding-pages"><h1>Adding Custom Pages</h1> +<p>To add your own custom pages, simply create a Markdown file under the <code>content/_pages</code> directory. PHPetite will take it from there!</p> +<h2>Some Caveats</h2> +<p>Any page you create will be automatically added to the <code>footer</code> navigation section. If you wish to hide individual pages from showing in the <code>footer</code>, do so via CSS:</p> +<pre><code class="css">footer a.slug-name-of-your-page { + display: none; +}</code></pre> +<p>If you want to remove the <code>footer</code> navigation altogether, add the following to your <code>style.css</code> file:</p> +<pre><code class="css">footer .footer-links { + display: none; +}</code></pre></section><section tabindex="0" role="document" aria-label="About PHPetite +" id="about"><h1>About PHPetite</h1> +<p>This entire website is a single <abbr title="Hyper Text Markup Language">HTML</abbr> file. It was generated by <a href="https://git.btxx.org/phpetite">PHPetite</a>.</p> +<p>If you notice any issues or want to help make this project even better, <a href="https://git.btxx.org/phpetite">check it out on cgit</a>.</p> +<h2>Feature Wishlist</h2> +<p>☐ Implement a "watch" system for local development (auto-rebuilds)<br> +☐ Detailed documentation for converting existing static sites to PHPetite<br> +☐ More theme / styling options!<br> +☐ Proper accessibility audit<br> +☑ <del>Allow custom fonts to be set as Base64 strings</del> (<a href="#2021-02-27-converting-custom-fonts-to-base64-strings">details here</a>)<br> +☑ <del>Set images as inline Base64 strings</del><br> +☑ <del>Basic RSS feed</del><br> +☑ <del>Automatically add new pages to footer nav</del><br> +☑ <del>Compress inline CSS</del></p></section> + <section tabindex="0" role="document" aria-label="Home" id="home"> + <div class="site-info"><h1>A Single File Blog</h1> +<p>PHPetite (/p/h/pəˈtēt/) is a single file, static blog generated from PHP. Based off the very minimal and awesome <a target="_blank" href="https://github.com/cadars/portable-php">portable-php</a></p> +<h2>Key Features</h2> +<ul> +<li>Entire blog is rendered in a single HTML file</li> +<li>Inline, compressed CSS</li> +<li>All images converted into base64 encoding</li> +<li>Minimal requirements / no heavy build tools</li> +</ul> +<hr> +<p>Feel free to look through the documentation found posted on this site or directly in the github repo.</p> +<h2>Getting Started</h2> +<ul> +<li><a href="#requirements">Requirements</a></li> +<li><a href="#generating-this-blog">Generating This Blog</a></li> +<li><a href="#structure">Structuring Blog Posts</a></li> +<li><a href="#adding-pages">Adding Custom Pages</a></li> +</ul></div><hr> + <nav> + <ul class="toc"> + <li><a href="#2022-06-28-cleaning-things-up-future-phpetite-updates"><span> Cleaning Things Up & Future PHPetite Updates +</span></a> <time datetime="2022-06-28">2022-06-28</time></li><li><a href="#2021-02-27-converting-custom-fonts-to-base64-strings"><span> Converting Custom Fonts to Base64 Strings +</span></a> <time datetime="2021-02-27">2021-02-27</time></li><li><a href="#2021-02-18-disable-image-to-base64-conversion"><span> Disable Image to Base64 Conversion +</span></a> <time datetime="2021-02-18">2021-02-18</time></li><li><a href="#2021-02-08-automatic-rss"><span> Automatic RSS +</span></a> <time datetime="2021-02-08">2021-02-08</time></li><li><a href="#2021-02-07-the-benefits-of-a-single-file-blog"><span> The Benefits of a Single File Blog +</span></a> <time datetime="2021-02-07">2021-02-07</time></li><li><a href="#2021-02-06-converting-from-jekyll"><span> Converting from Jekyll +</span></a> <time datetime="2021-02-06">2021-02-06</time></li><li><a href="#2021-01-09-markdown-examples"><span> Markdown examples +</span></a> <time datetime="2021-01-09">2021-01-09</time></li> + </ul> + </nav> + </section> + </main> + <footer> + <small>Last updated on February 2, 2024</small> + <div class="footer-links"> + <small><a class="structure" href="#structure">Structuring Blog Posts</a><span class="divider">/</span><a class="requirements" href="#requirements">Requirements</a><span class="divider">/</span><a class="home-content" href="#home-content">A Single File Blog</a><span class="divider">/</span><a class="generating-this-blog" href="#generating-this-blog">Generating This Blog</a><span class="divider">/</span><a class="adding-pages" href="#adding-pages">Adding Custom Pages</a><span class="divider">/</span><a class="about" href="#about">About PHPetite</a><span class="divider">/</span></small> + </div> + </footer> +</body> +</html> diff --git a/content/2021-01-09.md b/content/2021-01-09.md new file mode 100644 index 0000000..84f528c --- /dev/null +++ b/content/2021-01-09.md @@ -0,0 +1,45 @@ +# Markdown examples + +On top of plain Markdown, [Markdown Extra](https://michelf.ca/projects/php-markdown/extra) adds support for footnotes, abbreviations, definition lists, tables, `class` and `id` attributes, fenced code blocks, and Markdown inside HTML blocks. + +Additionally, images are properly enclosed in figure elements (with optional figcaption), and the `loading="lazy"` attribute is added. + +*** + +This is **bold**, *italic*, this is an [internal link](#2021-01-11-hello-world), this is ~~not~~ `code`, press <kbd>alt</kbd>. + +![This is the image alt text](content/img/image.png "This is the image title.") {width=1280 height=800} + This is the image caption (line begins with a space). The image above is actually a rendered base64 encoding. + +> This text is in a blockquote. + +## This is a level 2 heading + +### This is a level 3 heading + +- This +- is +- a list + +0. This +1. is +10. an +11. ordered list + +```.txt +This is + preformatted + text. +``` + +this is a table | header |this column is right-aligned | +----------------| --------------|----------------------------:| +these | content |1234 | +are | cells |56789 | + +This sentence has a footnote.[^1] + +[^1]: This is a footnote + +*[HTML]: Hypertext Markup Language +*[Markdown]: Markdown is a lightweight markup language for creating formatted text using a plain-text editor
\ No newline at end of file diff --git a/content/2021-02-06.md b/content/2021-02-06.md new file mode 100644 index 0000000..59f8d2f --- /dev/null +++ b/content/2021-02-06.md @@ -0,0 +1,3 @@ +# Converting from Jekyll + +This walkthrough is still being tweaked and optimized. Check back soon for the final version!
\ No newline at end of file diff --git a/content/2021-02-07.md b/content/2021-02-07.md new file mode 100644 index 0000000..01fe3da --- /dev/null +++ b/content/2021-02-07.md @@ -0,0 +1,23 @@ +# The Benefits of a Single File Blog + +Rendering your blog or website as a single file using PHPetite is pretty fantastic. It gives you the freedom to easily share, host or edit your site's content on almost any hosting provider. + +Since the entire blog's content is generated inline on `build`, you don't need to fiddle around with external `CSS` and `JS` files. There is also no need to worry about broken `img` sources since PHPetite converts all images into proper base64 strings. + +Using this website as an example: this blog weighs in at **~21KB**[^1]. + +That is incredibly tiny in terms of website size. Some sections on other web pages are larger than that! + +The portability of having a *single* `HTML` file as your blog is quite liberating. Though it should be noted, if your blog consists of high resolutions imagery or includes massive amounts of content, a single file might be a little impractical for you. + +## Hosting for Newcomers + +I suggest using [Netlify Drop](https://app.netlify.com/drop) if this is your first time setting up a hosting environment or you don't consider yourself too tech-savvy. Once you have your rendered `index.html` file, simply drag-and-drop the file into Netlify Drop - that's it! + +From there you can always setup a permanent subdomain or use your own custom domain. + +## Local Development + +Simply follow the instructions found on the [Generating This Blog](#generating-this-blog) page and you'll be running a local version of your site in seconds. + +[^1]: At this time of writing (Feb 2021) diff --git a/content/2021-02-08.md b/content/2021-02-08.md new file mode 100644 index 0000000..ba4db2d --- /dev/null +++ b/content/2021-02-08.md @@ -0,0 +1,13 @@ +# Automatic RSS + +PHPetite ships with a very basic and crude auto-generated RSS feed. When you run the project's build script: + +```.bash +bash build.sh +``` + +it not only generates the single HTML blog file, but also creates an `atom.xml` file in the root directory. Simply share this with your followers or link it somewhere on your site itself (eg. `yourdomain.com/atom.xml`). + +You can view this site's RSS feed here: + +[https://phpetite.org/atom.xml](https://phpetite.org/atom.xml) diff --git a/content/2021-02-18.md b/content/2021-02-18.md new file mode 100644 index 0000000..4234ff9 --- /dev/null +++ b/content/2021-02-18.md @@ -0,0 +1,12 @@ +# Disable Image to Base64 Conversion + +Some users[^1] may wish to host their imagery and media via a 3rd party source or simply want to avoid the heavy weight added with using base64 strings (~133%+ in size). + +To disable this feature, open your `_phpetite/_config.php` file and change the `images_to_base64` variable to false. + +```.php +// Activate or disable images to base64 strings +$images_to_base64 = false; +``` + +[^1]: Thanks to [Minor49er](https://news.ycombinator.com/user?id=Minor49er) for suggesting this option on [Hacker News](https://news.ycombinator.com/item?id=26175904)
\ No newline at end of file diff --git a/content/2021-02-27.md b/content/2021-02-27.md new file mode 100644 index 0000000..72f0dde --- /dev/null +++ b/content/2021-02-27.md @@ -0,0 +1,26 @@ +# Converting Custom Fonts to Base64 Strings + +There are currently no plans to automatically convert custom fonts to base64 strings within the project itself - **but** it is very easy to do so manually for Mac/Linux users. + +Simply open a terminal window and navigate to where your custom font file is located. The enter the following command (replacing the font extension name with your appropriate file name): + +```.bash +base64 your-custom-font.woff2 > font-base64.txt +``` + +Then in your `style.css` file, add the custom font as you normally would via the `@font-face` property but this time utilizing base64: + +```.css +@font-face { + font-family: 'FontName; + src: url(data:font/woff2;base64,[BASE64 CODE]) format('woff2'); +} +``` + +## Things to Keep in Mind + +Remember that by using base64 strings you are *significantly* increasing the overall size of your single file project. This should be used for extreme use cases where a single file website/blog isn't allowed access to 3rd party URLs or extra files on the root server. Hence why by default it isn't include in the PHPetite project itself. + +## Live Example + +You can check out the [ThriftyName](https://thrifty.name) project (built on PHPetite) to see base64 custom fonts in use.
\ No newline at end of file diff --git a/content/2022-06-28.md b/content/2022-06-28.md new file mode 100644 index 0000000..df4231d --- /dev/null +++ b/content/2022-06-28.md @@ -0,0 +1,27 @@ +# Cleaning Things Up & Future PHPetite Updates + +It has been quite a long time since I've reviewed or updated this little project of mine. Since it's release, I've created another minimal blogging system (based on `bash` this time) called [Shinobi](https://shinobi.website) and converted my personal website to use *that* instead. + +But I still love this single file blogging concept. So, I thought it was time for some basic cleanup. That "cleanup" slowly turned into a TODO list of sorts and now there are extra features I plan to add. + +## The Cleanup + +\* I first started by including the specific `$_SERVER['DOCUMENT_ROOT'].` parameter in the main PHP includes. I noticed when pulling the project in a "fresh" instance that the build failed without this setup. Sorry for anyone who may have ran into this issue previously! + +\* The default build script has been moved into a proper `makefile`. Now, generating the website only requires you to run `make` from the main directory. Running `make serve` builds the website and also runs a local server for testing at `localhost:8000`. Nothing groundbreaking but pretty helpful. + +\* The original dark mode CSS styling has been removed in favor of using the browser supported `color scheme` meta tag. + +\* Post dates are now listed at the top of each blog article (see above for reference) + +## What's to Come + +I keep a running list of features I plan to implement (in no particular order) on the main [about section](#about). Feel free to open an issue on the [official sourcehut todo](https://todo.sr.ht/~bt/phpetite) if you have other features and suggestions. Don't be shy! + +## Follow Along + +Rolling out any new updates for this project will take time. I'm in no *real* rush and I do have other projects that require my attention. That said, if you wish to stay up-to-date, I recommend following along via the [official RSS feed](/atom.xml). + +Thanks for reading and happy single-file blogging! + +-- Brad
\ No newline at end of file diff --git a/content/_drafts/1986-04-16.md b/content/_drafts/1986-04-16.md new file mode 100644 index 0000000..8f9b9a4 --- /dev/null +++ b/content/_drafts/1986-04-16.md @@ -0,0 +1,147 @@ +# Against Joie de Vivre + +by Philip Lopate. + +Over the years I have developed a distaste for the spectacle of *joie de vivre*, the knack of knowing how to live. Not that I disapprove of all hearty enjoyment of life. A flushed sense of happiness can overtake a person anywhere, and one is no more to blame for it than the Asiatic flu or a sudden benevolent change in the weather (which is often joy's immediate cause). No, what rankles me is the stylization of this private condition into a bullying social ritual. + +The French, who have elevated the picnic to their highest civilized rite, are probably most responsible for promoting this smugly upbeat, flaunting style. It took the French genius for formalizing the informal to bring sticky sacramental sanctity to the baguette, wine and cheese. A pure image of sleeveless *joie de vivre* Sundays can also be found in Renoir's paintings. Weekend satyrs dance and wink; leisure takes on a bohemian stripe. A decent writer, Henry Miller, caught the French malady and ran back to tell us of pissoirs in the Paris streets (why this should have impressed him so, I've never figured out). + +But if you want a double dose of *joie de vivre*, you need to consult a later, hence more stylized version of the French myth of pagan happiness: those *Family of Man* photographs of endlessly kissing lovers, snapped by Doisneau and Boubat, not tomention Cartier-Bresson's icon of the proud tyke carrying bottles of wine. If Cartier Bresson and his disciples are excellent photographers for all that, it is in spite of their rubbing our noses in a tediously programmatic "affirmation of life." + +Though it is traditionally the province of the French, the whole Mediterranean is a hotbed of professional *joie de vivrism*, which they have gotten down to a routine like a crack *son et lumière* display. The Italians export *dolce far niente* as aggressively as tomato paste. For the Greeks, a Zorba dance to life has supplanted classical antiquities as their main touristic lure. Hard to imagine anything as stomach-turning as being forced to participate in such an oppressively robust, folknik effusion. Fortunately, the country has its share of thin, nervous, bitter types, but Greeks do exist who would clutch you to their joyfully stout bellies and crush you there. The *joie de vivrist* is an incorrigible missionary, who presumes that everyone wants to express pro-life feelings in the same stereotyped manner. + +A warning: since I myself have a large store of nervous discontent (some would say hostility) I am apt to be harsh in my secret judgments of others, seeing them as defective because they are not enough like me. From moment to moment, the person I am with often seems too shrill, too bland, too something-or-other to allow my own expansiveness to swing into stage center. "Feeling no need to drink, you will promptly despise a drunkard" (Kenneth Burke). So it goes with me—which is why I am not a literary critic. I have no faith that my discriminations in taste are anything but the picky awareness of what will keep me stimulated, based on the peculiar family and class circumstances which formed me. But the knowledge that my discriminations are skewed and not always universally desirable doesn't stop me in the least from making them, just as one never gives up a negative first impression, no matter how many times it is contradicted. A believer in astrology (to cite another false system), having guessed that someone is aSaggitarius, and then told he is a Scorpio, says "Scorpio—yes, of course!" without missing a beat, or relinquishing confidence in his ability to tell people's signs, or in his idea that the person is somehow secretly Saggitarian. + +## The Houseboat + +I remember the exact year when my dislike for *joie de vivre* began to crystallize. It was 1969. We had gone to visit an old Greek painter on his houseboat in Sausalito. Old Vartas's vitality was legendary and it was considered a spiritual honor to meet him, like getting an audience with the Pope. Each Sunday he had a sort of open house, or open boat. + +My "sponsor," Frank, had been many times to the houseboat, furnishing Vartas with record albums, since the old painter had a passion for San Francisco rock bands. Frank told me that Vartas had been a pal of Henry Miller's, and I, being a writer of Russian descent, would love him. I failed to grasp the syllogism, but, putting aside my instinct to dislike anybody I have been assured I will adore, I prepared myself to give the man a chance. + +Greeting us on the gang plank was an old man with thick, lush white hair and snowy eyebrows, his face reddened from the sun. As he took us into the houseboat cabin he told me proudly that he was seventy-seven years old, and gestured toward the paintings that were spaced a few feet apart, leaning on the floor against the wall. They were celebrations of the blue Aegean, boats moored in ports, whitewashed houses on a hill, painted in primary colors and decorated with collaged materials: mirrors, burlap, lifesaver candies. These sunny little canvases with their talented innocence, third generation spirit of Montmartre, bore testimony to a love of life so unbending as to leave an impression of rigid narrow-mindedness as extreme as any Savonarola. Their rejection of sorrow was total. They were the sort of festive paintings that sell at high-rent Madison Avenue galleries specializing in European schlock. + +Then I became aware of three young, beautiful women, bare-shouldered, wearing white dhotis, each with long blond hair falling onto a skyblue halter—unmistakably suggesting the Three Graces. They lived with him on the houseboat, I was told, giving no one knew what compensation for their lodgings. Perhaps their only payment was to feed his vanity in front of outsiders. The Greek painter smiled with the air of an old fox around the trio. For their part, they obligingly contributed their praises of Vartas's youthful zip, which of course was taken by some guests as double-entendre for undiminished sexual prowess. The Three Graces also gathered the food-offerings of the visitors to make a midday meal. + +Then the boat, equipped with a sail, was launched to sea. I must admit it gave me a spoilsport's pleasure when the winds turned becalmed. We could not move. Aboard were several members of the Bay Area's French colony, who dangled their feet over the sides, passed around bunches of grapes and sang what I imagined were Gallic camping songs. The French know boredom, so they would understand how to behave in such a situation. It has been my observation that many Frenchmen and women stationed in America have the attitude of taking it easy, slumming at a health resort, and nowhere more so than in California. The *émigré* crew included a securities analyst, an academic sociologist, a museum administrator and his wife, a modiste: on Vartas's boat they all got drunk and carried on like redskins, noble savages off Tahiti. + +*Joie de vivre* requires a *soupçon* of the primitive. But since the illusion of the primitive soon palls and has nowhere to go, it becomes necessary to make new initiates. A good part of the day, in fact, was taken up with regulars interpreting to first-timers like myself certain mores pertaining to the houseboat, as well as offering tidbits about Vartas's Rabelaisian views of life. Here everyone was encouraged to do what he willed. (How much could you do on a becalmed boat surrounded by strangers?) No one had much solid information about their host's past, which only increased the privileged status of those who knew at least one fact. Useless to ask the object of this venerating speculation, since Vartas said next to nothing (adding to his impressiveness) when he was around, and disappeared below for long stretches of time. + +In the evening, after a communal dinner, the new Grateful Dead record Frank had brought was put on the phonograph, and Vartas danced, first by himself, then with all three Graces, bending his arms in broad, hooking sweeps. He stomped his foot and looked around scampishly at the guests for appreciation, not unlike a monkey-grinder and his monkey. Imagine, if you will, a being whose generous bestowal of self-satisfaction invites and is willing to receive nothing but flattery in return, a person who has managed to make others buy his somewhat senile projection of indestructibility as a Hymn to Life. In no sense could he be called a charlatan; he delivered what he promised, an incaination of* joie de vivre*, and if it was shallow, it was also effective, managing even to attract an enviable "harem" (which was what really burned me). + +A few years passed. + +Some Dutch TV crew, ever on the lookout for exotic bits of Americana that would make good short subjects, planned to do a documentary about Vartas as a sort of paean to eternal youth. I later learned from Frank that Vartas died before the shooting could be completed. A pity, in a way. The home movie I've run off in my head of the old man is getting a little tattered, the colors splotchy, and the scenario goes nowhere, lacks point. All I have for sure is the title: *The Man Who Gave *Joie De Vivre* A Bad Name*. + +"Ah, what a twinkle in the eye the old man has! He'll outlive us all." So we speak of old people who bore us, when we wish to honor them. We often see projected onto old people this worship of the life-force. It is not the fault of the old if they then turn around and try to exploit our misguided amazement at their longevity as though it were a personal tour de force. The elderly, when they are honest with themselves, realize they have done nothing particularly to be proud of in lasting to a ripe old age, and then carrying themselves through a thousand more days. Yet you still hear an old woman or man telling a bus driver with a chuckle, "Would you believe that I am eighty-four years old!" As though they should be patted on the back for still knowing how to talk, or as though they had pulled a practical joke on the other riders by staying so spry and mobile. Such insecure, wheedling behavior always embarrassed me. I will look away rather than meet the speaker's eyes and be forced to lie with a smile, "Yes, you are remarkable," which seems condescending on my part and humiliating to us both. + +Like children forced to play the cute part adults expect of them, some old people must get confused trying to adapt to a social role of indeterminate standards, which is why they seem to whine: "I'm doing all right, aren't I—for my age?" It is interesting that society's two most powerless groups, children and the elderly, have both been made into sentimental symbols. In the child's little hungry hands grasping for life, joined to the old person's frail slipping fingers hanging onto it, you have one of the commonest advertising metaphors for intense appreciation. It is enough to show a young child sleeping in his or her grandparent's lap to procure *joie de vivre* overload. + +## The Dinner Party + +I am invited periodically to dinner parties and brunches—and I go, because I like to be with people and oblige them, even if I secretly cannot share their optimism about these events. I go, not believing that I will have fun, but with the intent of observing people who think a *dinner party* a good time. I eat their fancy food, drink the wine, make my share of entertaining conversation, and often leave having had a pleasant evening. Which does not prevent me from anticipating the next invitation with the same bleak lack of hope. To put it in a nutshell, I am an ingrate. + +Although I have traveled a long way from my proletarian origins and, like a perfect little bourgeois, talk, dress, act and spend money, I hold onto my poor-boy's outrage at the "decadence" (meaning, dull entertainment style) of the middle and upper-middle classes; or, like a model Soviet moviegoer watching scenes of pre-revolutionary capitalists gorging caviar, I am appalled, but I dig in with the rest. + +Perhaps my uneasiness with dinner parties comes from the simple fact that not a single dinner party was given by my solitudinous parents the whole time I was growing up, and I had to wait until my late twenties before learning the ritual. A spy in the enemy camp, I have made myself a patient observer of strange customs. For the benefit of other late starting social climbers, this is what I have observed: + +As everyone should know, the ritual of the dinner party begins away from the table. Usually in the living room, hors d'oeuvres and walnuts are set out, to start the digestive juices flowing. Here introductions between strangers are also made. Most dinner parties contain at least a few guests who have been unknown to each other before that evening, but whom the host and/or hostess envision would enjoy meeting. These novel pairings and their interactions add spice to the post-mortem: who got along with whom? The lack of prior acquaintanceship also ensures that the guests will have to rely on and go through the only people known to everyone, the host and hostess, whose absorption of this helplessly dependent attention is one of the main reasons for throwing dinner parties. + +Although an after-work "leisure activity," the dinner party is in fact a celebration of professional identity. Each of the guests has been pre-selected as in a floral bouquet; and in certain developed forms of this ritual there is usually a cunning mix of professions. Yet the point is finally not so much diversity as commonality: what remarkably shared attitudes and interests these people from different vocations demonstrate by conversing intelligently, or at least glibly, on the topics that arise. Naturally, a person cannot discourse too technically about one's line of work, so he or she picks precisely those themes that invite overlap. The psychiatrist laments the new breed of ego-less, narcissistic patient who keeps turning up in his office—a beach bum who lacks the work ethic; the college professor bemoans the shoddy intellectual backgrounds and self centered ignorance of his students; and the bookseller parodies the customer who pronounced "Sophocles" to rhyme with "bifocles." The dinner party is thus an exercise inlocating ignorance—elsewhere. Whoever is present is ipso facto part of that beleaguered remnant of civilized folk fast disappearing from Earth. + +Or think of a dinner party as a club of revolutionaries, a technocratic elite whose social interactions that night are a dry run for some future takeover of the State. These are the future cabinet members (now only a shadow-cabinet, alas) meeting to practice for the first time. How well they get on! "The time will soon be ripe, my friends. . . ." If this is too fanciful for you, then compare the dinner party to a utopian community, a Brook Farm supper club, where only the best and most useful community-members are chosen to participate. The smugness begins as soon as one enters the door, since one is already part of the chosen few. And from then on, every mechanical step in dinner-party process is designed to augment the atmosphere of group *amour-propre*. This is not so say that there won't be one or two people in an absolute torment of exclusion, too shy to speak up, or else suspecting that when they do, their contributions fail to carry the same weight as the others'. The group's all-purpose drone of self-contentment ignores these drowning people—cruelly inattentive in one sense, but benign in another: it invites them to join the shared ethos of success any time they are ready. + +The group is asked to repair to the table. Once again they find themselves marvelling at a shared perception of life. How delicious the fish soup! How cute the stuffed tomatoes! What did you use for this green sauce? Now comes much talk of ingredients, and creditis given where credit is due. It is Jacques who made the salad. It was Mamie who brought the homemade bread. Everyone pleads with the hostess to sit down, not to work so hard—an empty formula whose hypocrisy bothers no one. Who else is going to put the butter dish on the table? For a moment all become quiet, except for the sounds of eating. This corresponds to the part in a church service which calls for silent prayer. + +I am saved from such culinary paganism by the fact that food is largely an indifferent matter to me. I rarely think much about what I am putting in my mouth. Though my savage, illiterate palate has inevitably been educated to some degree by the many meals I have shared with people who care enormously about such things, I resist going any further. I am superstitious that the day I send back a dish at a restaurant, or make a complicated journey to somewhere just for a meal, that day I will have sacrificed my freedom and traded in my soul for a lesser god. + +I don't expect the reader to agree with me. That's not the point. Unlike the behavior called for at a dinner party, I am not obliged sitting at my typewriter to help procure consensus every moment. So I am at liberty to declare, to the friend who once told me that dinner parties were one of the only opportunities for intelligently convivial conversation to take place in this cold, fragmented city, that she is crazy. The conversation at dinner parties is of a mind-numbing calibre. No discussion of any clarifying rigor—be it political, spiritual, artistic or financial—can take place in a context where fervent conviction of any kind is frowned upon, and the desire to follow through asequence of ideas must give way every time to the impressionistic, breezy flitting from topic to topic. Talk must be bubbly but not penetrating. Illumination would only slow the flow. Some hit-and-run remark may accidentally jog an idea loose, but in such cases it is better to scribble a few words down on the napkin for later, than attempt to "think" at a dinner party. + +What do people talk about at such gatherings? The latest movies, the priciness of things, word-processors, restaurants, muggings and burglaries, private versus public schools, the fool in the White House (there have been so many fools in a row that this subject is getting tired), the underserved reputations of certain better-known professionals in one's field, the fashions in investments, the investments in fashion. What is traded at the dinner-party table is, of course, class information. You will learn whether you are in the avant-garde or rear guard of your social class, or, preferably, right in step. + +As for Serious Subjects, dinner-party guests have the latest New Yorker in-depth piece to bring up. People who ordinarily would not spare a moment worrying about the treatment of schizophrenics in mental hospitals, the fate of Great Britain in the Common Market, or the disposal of nuclear wastes, suddenly find their consciences orchestrated in unison about these problems, thanks to their favorite periodical—though a month later they have forgotten all about it and are onto something new. + +The dinner party is a suburban form of entertainment. Its spread in our big cities represents an insidious Fifth Column suburbanization of the metropolis. In the suburbs it becomes necessary to be able to discourse knowledgeably about the heart of the city, but from the viewpoint of a day-shopper. Dinner-party chatter is the communicative equivalent of roaming around shopping malls. + +Much thought has gone into the ideal size for a dinner party—usually with the hostess arriving at the figure eight. Six would give each personality too much weight; ten would lead to splintering side-discussions; eight is the largest number still able to force everyone into the same compulsively congenial conversation. My own strength as a conversationalist comes out less in groups of eight than one-to-one, which may explain my resistance to dinner parties. At the table, unfortunately, any engrossing *tête-à-tête* is frowned upon as anti-social. I often find myself in the frustrating situation of being drawn to several engaging people, in among the bores, and wishing I could have a private conversation with each, without being able to do more than signal across the table a wry recognition of that fact. "Some other time, perhaps," we seem to be saying with our eyes, all evening long. + +Later, however—to give the devil his due—when guests and hosts retire from the table back to the living room, the strict demands of group participation may be relaxed, and individuals allowed to pair off in some form of conversational intimacy. But one must be ever on the lookout for the group's need to swoop everybody together again for one last demonstration of collective fealty. + +The first to leave breaks the communal spell. There is a sudden rush to the coat closet, the bathroom, the bedroom, as others, under the protection of the first defector's original sin, quit the Party apologetically. The utopian dream has collapsed: left behind are a few loyalists and insomniacs, swillers of a last cognac. "Don't leave yet," begs the host, knowing what a sense of letdown, pain and self-recrimination awaits. Dirty dishes are, if anything, a comfort: the faucet's warm gush serves to stave off the moment of anesthetized stock-taking—Was that really necessary?—in the sobering silence which follows a dinner party. + +## Joie's Doppelgänger + +I have no desire to rail against the Me Generation. We all know that the current epicurean style of the Good Life, from light foods to Nike running shoes, is a result of market research techniques developed to sell "spot" markets, and, as such, a natural outgrowth of consumer capitalism. I may not like it but I can't pretend that my objections are the result of a highminded Laschian political analysis. Moreover, my own record of activism is not so noticeably impressive that I can lecture the Sunday brunchers to roll up their sleeves and start fighting social injustices instead of indulging themselves. + +No, if I try to understand the reasons for my antihedonistic biases, they come from somewhere other than idealism. It's odd, because there seems to be a contradiction between this curmudgeonly feeling inside me and my periodically strong appetite for life. I am reminded of my hero, William Hazlitt, with his sarcastic grumpy disposition on the one hand, and his capacity for "gusto" (his word, not Schlitz's) on the other. With Hazlitt, one senses a fanatically tenacious defense of his individuality and independence against some unnamed bully stalking him. He had trained himself to be a connoisseur of vitality, and got irritated when life was not filled to the brim. I am far less irritable—before others; I will laugh if there is the merest *anything* to laugh at. But it is a tense, pouncing pleasure, not one which will allow me to sink into undifferentiated relaxation. The prospect of a long day at the beach makes me panic. There is no harder work I can think of than taking myself off to somewhere pleasant, where I am forced to stay for hours and "have fun." Taking it easy, watching my personality's borders loosen and dissolve, arouses an unpleasantly floating giddiness. I don't even like water-beds. Fear of Freud's "oceanic feeling," I suppose. . . .I distrust anything which will make me pause long enough to be put in touch with my helplessness. + +The other repugnance I experience around *joie-de-vivrism* is that I associate its rituals with depression. All these people sitting around a pool, drinking margaritas, they're not really happy, they're depressed. Perhaps I am generalizing too much from my own despair in such situations. Drunk, sunbaked, stretched out in a beach-chair, I am unable to ward off the sensation of being utterly alone, unconnected, cut off from the others. + +An article on the Science Page of the *Times* about depression (they seem to run one every few months) described the illness as a pattern of "learned helplessness." Dr. Martin Seligman of the University of Pennsylvania described his series of experiments: "At first mild electrical shocks were given to dogs, from which they were unable to escape. In a second set of experiments, dogs were given shocks from which they could escape—but they didn't try. They just lay there, passively accepting the pain. It seemed that the animals' inability to control their experiences had brought them to a state resembling clinical depression in humans." + +Keep busy, I always say. At all costs avoid the trough of passivity, which leads to the Slough of Despond. Someone—a girlfriend, who else?—once accused me of being intolerant of the depressed way of looking at the world, which had its own intelligence and moral integrity, both obviously unavailable to me. It's true. I don't like the smell of depression (it has a smell, a very distinct one, something fetid like morning odors), and I stay away from depressed characters whenever possible. Except when they happen to be my closest friends or family members. It goes without saying that I am also, for all my squeamishness, attracted to depressed people, since they seem to know something I don't. I wouldn't rule out the possibility that the brown-gray logic of depression is the truth. In another experiment (also reported in the *Time'*s Science section), pitting "optimists" against clinically diagnosed "depressives" on their self-perceived abilities to effect outcomes according to their wills, researchers tentatively concluded that depressed people may have a more realistic, clear-sighted view of the world. + +Nevertheless, what I don't like about depressives sometimes is their chummy I-told-you so smugness, like Woody Allen fans who treat acedia as a vanguard position. + +And for all that, depressives make the most rabid converts to *joie de vivre*. The reason is, *joie de vivre* and depression are not opposites but relatives of the same family, practically twins. When I see *joie de vivre* rituals, I always notice, like a TV ghost, depression right alongside it. I knew a man, dominated by a powerful father, who thought he had come out of a long depression occasioned, in his mind, by his divorce. Whenever I met him he would say that his life was getting better and better. Now he could run long distances, he was putting healthy food in his system, he was more physically fit at forty than he had been at twenty-five, and now he had dates, he was going out with three different women, he had a good therapist, he was looking forward to renting a bungalow in better woods than the previous summer. . . .I don't know whether it was his tone of voice when he said this, his sagging shoulders, or what, but I always had an urge to burst into tears. If only he had admitted he was miserable I could have consoled him outright instead of being embarrassed to notice the deep hurt in him, like a swallowed razor cutting him from inside. And his pain still stunk up the room like in the old days, that sour cabbage smell was in his running suit, yet he wouldn't let on, he thought the smell was gone. The therapist had told him to forgive himself, and he had gone ahead and done it, the poor shlemiehl. But tell me: why would anyone need such a stylized, disciplined regimen of enjoyment if he were not depressed? + +## In the Here-And-Now + +The argument of both the hedonist and the guru is that if we were but to open ourselvesto the richness of the moment, to concentrate on the feast before us, we would be filled with bliss. I have lived in the present from time to time, and I can tell you that it is much over-rated. Occasionally, as a holiday from stroking one's memories or brooding about future worries, I grant you, it can be a nice change of pace. But to "be here now" hour after hour would never work. I don't even approve of stories written in the present tense. As for poets who never use a past participle, they deserve the eternity they are striving for. + +Besides, the present has a way of intruding whether you like it or not; why should I go out of my way to meet it? Let it splash on me from time to time, like a car going through a puddle, and I, on the sidewalk of my solitude, will salute it grimly like any other modern inconvenience. + +If I attend a concert, obviously not to listen to the music but to find a brief breathing space in which to meditate on the past and future, I realize that there may be moments when the music invades my ears and I am forced to pay attention to it, note after note. I believe I take such intrusions gracefully. The present is not always an unwelcome guest, so long as it doesn't stay too long and cut into our time for remembering. + +Even for survival, it's not necessary to focus one's full attention on the present. The instincts of a pedestrian crossing the street in a reverie will usually suffice. Alertness is alright as long as it is not treated as a promissory note on happiness. Anyone who recommends attention to the moment as a prescription for grateful wonder is only telling half the truth. To be happy one must pay attention, but to be unhappy one must also have paid attention. + +Attention, at best, is a form of prayer. Conversely, as Simone Weil said, prayer is a way of focusing attention. All religions recognize this when they ask their worshipers to repeat the name of their God, a devotional practice which draws the practitioner into a trancelike awareness of the present, and the objects around oneself. With a part of the soul one praises God, and with the other part one expresses a hunger, a dissatisfaction, a desire for more spiritual contact. Praise must never stray too far from longing, that longing which takes us implicitly beyond the present. + +I was about to say that the very act of attention implies longing, but this is not necessarily true. Attention is not always infused with desire; it can settle on us most placidly once desire has been momentarily satisfied, like after the sex act. There arealso periods following over-work, when the exhausted slavebody is freed and the eyes dilate to register with awe the lights of the city; one is too tired to desire anything else. + +Such moments are rare. They form the basis for a poetic appreciation of the beauty of the world. However, there seems no reliable way to invoke or prolong them. The rest of the time, when we are not being edgy or impatient, we are often simply *disappointed*, which amounts to a confession that the present is not good enough. People often try to hide their disappointment—just as Berryman's mother told him not to let people see that he was bored, because it suggested that he had no "inner resources." But there is something to be said for disappointment. + +This least respected form of suffering, downgraded to a kind of petulance, at least accurately measures the distance between hope and reality. And it has its own peculiar satisfactions: Why else do we return years later to places where we had been happy, if not to savor the bittersweet pleasure of disappointment? "For you well know: while a single disappointment may elicit tears, a repeated disappointment will evoke a smile" (Musil). + +Moreover, it is the other side of a strong, predictive feeling for beauty or appropriate civility or decency: Only those with a sense of order and harmony can be disappointed. + +We are told that to be disappointed is immature, in that it presupposes having unrealistic expectations, whereas the wise man meets each moment head-on without preconceptions, with freshness and detachment, grateful for anything it offers. However, this pernicious teaching ignores everything we know of the world. If we continue to expect what turns out to be not forthcoming, it is not because we are unworldly in our expectations, but because our very worldliness has taught us to demand of an unjustworld that it behave a little more fairly. The least we can do, for instance, is to register the expectation that people in a stronger position be kind and not cruel to those in a weaker, knowing all the while that we will probably be disappointed. + +The truth is, most wisdom is embittering. The task of the wise person cannot be to pretend with false naiveté that every moment is new and unprecedented, but to bear the burden of bitterness which experience forces on us with as much uncomplaining dignity 0as strength will allow. Beyond that, all we can ask of ourselves is that bitterness not cancel out our capacity still to be surprised. + +## Making Love + +If it is true that I have the tendency to withhold sympathy from those pleasures or experiences which fall outside my capabilities, the opposite is also true: I admire immoderately those things I cannot do. I've always gone out with women who swam better than I did. It's as if I were asking them to teach me how to make love. Though I know how to make love (more or less), I have never fully shaken that adolescent boy's insecurity that there was more to it than I could ever imagine, and that I needed a full time instructress. For my first sexual experiences, in fact, I chose older women. Later, when I slept with women my own age and younger, I still tended to take the stylistic lead from them, adapting myself to each one's rhythm and ardor, not only because I wanted to be "responsive," but because I secretly thought that women—any woman—understood love-making in a way that I did not. In bed I came to them as a student; and I have made them pay later, in other ways, for letting them see me thus. Sex has always been so impromptu, so out of my control, so different each time, that even when I became the confident bull in bed I was dismayed by this surprising sudden power, itself a form of powerlessness because so unpredictable. + +Something Michel Leiris wrote in his book, *Manhood*, has always stuck with me: "It has been some time, in any case, since I have ceased to consider the sexual act as a simple matter, but rather as a relatively exceptional act, necessitating certain inner accommodations that are either particularly tragic or particularly exalted, but very different, in either case, from what I regard as my usual disposition." + +The transformation from a preoccupied urban intellectual to a sexual animal involves, at times, an almost superhuman strain. To find in one's bed a living, undulating woman of God knows what capacities and secret desires, may seem too high, too formal, too ridiculous or blissful an occasion—not to mention the shock to an undernourished heart like mine of an injection of undiluted affection, if the woman proves loving as well. + +Most often, I simply do what the flood allows me to, improvising here or there like a man tying a white flag to a raft that is being swiftly swept along, a plea for love or forgiveness. But as for artistry, control, enslavement through my penis, that's someone else. Which is not to say that there weren't women who were perfectly happy with me as a lover. In those cases, there was some love between us outside of bed: the intimacy was much more intense because we had something big to say to each other before we ever took off our clothes, but which could now be said only with our bodies. + +With other women, whom I cared less about, I was sometimes a dud. I am not one of those men who can force himself to make love passionately or athletically when his affections are not engaged. From the perplexity of wide variations in my experiences I have been able to tell myself that I am neither a good nor a bad lover, but one who responds differently according to the emotions present. A banal conclusion; maybe a true one. + +It does not do away, however, with some need to have my remaining insecurities about sexual ability laid to rest. I begin to suspect that all my fancy distrust of hedonism comes down to a fear of being judged in this one category: Do I make love well? Every brie and wine picnic, every tanned body relaxing on the beach, every celebration of *joie de vivre* carries a sly wink of some missed sexual enlightenment which may be too threatening to me. I am like the prudish old maid who blushes behind her packages when she sees sexy young people kissing. + +When I was twenty I married. My wife was the second woman I had ever slept with. Our marriage was the recognition that we suited one another remarkably well as company—could walk and talk and share insights all day, work side by side like Chinese peasants, read silently together like graduate students, tease each other like brother and sister, and when at night we found our bodies tired, pull the covers over ourselves and become lovers. She was two years older than I, but I was good at faking maturity; and I found her so companionable and trustworthy and able to take care of me that I could not let such a gold mine go by. + +Our love-life was mild and regular. There was a sweetness to sex, as befitted domesticity. Out of the surplus energy of late afternoons I would find myself coming up behind her sometimes as she worked in the kitchen, taking her away from her involvements, leading her by the hand into the bedroom. I would unbutton her blouse. I would stroke her breasts, and she would get a look in her eyes of quiet intermittent hunger, like a German shepherd being petted; she would seem to listen far off; absentmindedly day-dreaming, she would return my petting, stroke my arm with distracted patience like a mother who has something on the stove, trying to calm her weeping child.I would listen too to guess what she might be hearing, bird calls or steam heat. The enlargement of her nipples under my fingers fascinated me. Goose bumps either rose on her skin where I touched or didn't, I noted with scientific interest, a moment before getting carried away by my own eagerness. Then we were undressing, she was doing something in the bathroom, and I was waiting on the bed, with all the consciousness of a sunmote. I was large and ready. The proud husband, waiting to receive my treasure. . . . + +I remember our favorite position was she on top, I on the bottom, upthrusting and receiving. Distraction, absent-mindedness, return, calm exploration marked our sensual life. To be forgetful seemed the highest grace. We often achieved perfection. + +Then I became haunted with images of seductive, heartless cunts. It was the era of the miniskirt, girl-women, Rudi Gernreich bikinis and Tiger Morse underwear, see-through blouses, flashes of flesh which invited the hand to go creeping under and into costumes. I wanted my wife to be more glamorous. We would go shopping for dresses together, and she would complain that her legs were wrong for these new fashions. Or she would come home proudly with a bargain pink and blue felt minidress, bought for three dollars at a discount store, which my aching heart would tell me missed the point completely. + +She too became dissatisfied with the absence of furtive excitement in our marriage. She wanted to seduce me, like a stranger on a plane. But I was too easy, so we ended up seducing others. Then we turned back to each other and with one last desperate attempt, before the marriage fell to pieces, sought in the other a plasticity of sensual forms, like the statuary in an Indian temple. In our lovemaking I tried to believe that the body of one woman was the body of all women, and all I achieved was a groping to distance lovingly familiar forms into those of anonymous erotic succubi. The height of this insanity, I remember, was one evening in the park when I pounded my wife's lips with kisses in an effort to provoke something between us like "hot passion." My eyes close, I practiced a repertoire of French tongue-kisses on her. I shall never forget her frightened silent appeal that I stop, because I had turned into someone she no longer recognized. + +But we were young. And so, dependent on each other, like orphans. By the time I left, at twenty-five, I knew I had been a fool, and had ruined everything, but I had to continue being a fool because it had been my odd misfortune to have stumbled onto kindness and tranquility too quickly. + +I moved to California in search of an earthly sexual paradise, and that year I tried hardest to make my peace with *joie de vivre*. I was sick but didn't know it—a diseased animal, Nietzsche would say. I hung around Berkeley's campus, stared up at the Campanile tower, I sat on the grass watching coeds younger than I, and, pretending that I was still going to university (no deeper sense of being a fraud obtainable), I tried to grasp the rhythms of carefree youth; I blended in at rallies, I stood at the fringes of be-ins, watching new rituals of communal love, someone being passed through the air hand to hand. But I never "trusted the group" enough to let myself be the guinea pig; or if I did, it was only with the proud stubborn conviction that nothing could change me—though I also wanted to change. Swearing I would never learn transcendence, I hitchhiked and climbed mountains. I went to wine-tasting festivals, and also accepted the wine jug from hippie gypsies in a circle around a beach campfire, without first wiping off the lip. I registered for a Free School course in human sexual response, just to get laid; and when that worked, I was shocked, and took up with someone else. There were many women in those years who got naked with me. I wish I could remember their names. I smoked grass with them, and as a sign of faith I took psychedelic drugs, and we made love in bushes and beach-houses, as though hacking through jungles with machetes to stay in touch with our ecstatic genitals while our minds soared off into natural marvels. Such experiences taught me, I will admit, how much romantic feeling can transform the body whose nerve-tendrils are receptive to it. Technicolor fantasies of one girlfriend as a señorita with flowers in her impossibly wavy hair would suddenly pitch and roll beneath me, and the bliss of touching her naked suntanned breast and the damp black public hairs was too unthinkably perfect to elicit anything but abject gratitude. At such moments I have held the world in my hands and known it. I was coming home to the body of Woman, those globes and grasses which had launched me. In the childish fantasy accompanying one sexual climax, under LSD, I was hitting a home run, and the Stars and Stripes flying in the background of my mind's eye as I "slid into home" acclaimed the patriotic rightness of my semenal release. For once I had no guilt about how or when I ejaculated. + +If afterwards, when we came down, there was often a sour air of disenchantment and mutual prostitution, that does not take away from the legacy, the rapture of those moments. If I no longer use drugs—in fact, have become anti-drug—I think I still owe them something for showing me how to recognize the all-embracing reflex. At first I needed drugs to teach me about the stupendousness of sex. Later, without them, there would be situations—after a lovely talk or coming home from a party in a taxi—when I would be overcome by amorous tropism towards the woman with me. The appetite for flesh which comes over me at such moments, and the pleasure there is in finally satisfying it, seems so just that I always think I have stumbled into a state of blessed grace. That it can never last, that it is a trick of the mind and the blood, are rumors I push out of sight. + +To know rapture is to have one's whole life poisoned. If you will forgive a ridiculous analogy, a tincture of rapture is like a red bandana in the laundry that runs and turns all the white wash pink. We should just as soon stay away from any future ecstatic experiences which spoil everyday living by comparison. Not that I have any intention of stopping. Still, if I will have nothing to do with religious mysticism, it is probably because I sense a susceptibility in that direction. Poetry is also dangerous. All quickening awakenings to Being extract a price later. + +Are there people who live under such spells all the time? Was this the secret of the idiotic smile on the half-moon face of the painter Vartas? The lovers of life, the robust Cellinis, the Casanovas? Is there a technique to hedonism that will allow the term of rapture to be indefinitely extended? I don't believe it. The hedonist's despair is still that he is forced to make do with the present. Who knows about the success rate of religious mystics? In any case, I could not bring myself to state that what I am waiting for is God. Such a statement would sound too grandiose and presumptuous, and make too great a rupture in my customary thinking. But I can identify with the pre- if not the post-stage of what Simone Weil describes: + +"The soul knows for certain only that it is hungry. The important thing is that it announces its hunger by crying. A child does not stop crying if we suggest to it that perhaps there is no bread. It goes on crying just the same. The danger is not lest the soul should doubt whether there is any bread, but lest, by a lie, it should persuade itself that it is not hungry." + +So much for *joie de vivre*. It's too compensatory. I don't really know what I'm waiting for. I know only that until I have gained what I want from this life, my expressions of gratitude and joy will be restricted to variations of a hunter's alertness. I give thanks to a nip in the air that clarifies the scent. But I think it hypocritical to pretend satisfaction while I am still hungry.
\ No newline at end of file diff --git a/content/_pages/about.md b/content/_pages/about.md new file mode 100644 index 0000000..f0846f9 --- /dev/null +++ b/content/_pages/about.md @@ -0,0 +1,19 @@ +# About PHPetite + +This entire website is a single HTML file. It was generated by [PHPetite](https://git.btxx.org/phpetite). + +If you notice any issues or want to help make this project even better, [check it out on cgit](https://git.btxx.org/phpetite). + +## Feature Wishlist + +☐ Implement a "watch" system for local development (auto-rebuilds) +☐ Detailed documentation for converting existing static sites to PHPetite +☐ More theme / styling options! +☐ Proper accessibility audit +☑ ~~Allow custom fonts to be set as Base64 strings~~ ([details here](#2021-02-27-converting-custom-fonts-to-base64-strings)) +☑ ~~Set images as inline Base64 strings~~ +☑ ~~Basic RSS feed~~ +☑ ~~Automatically add new pages to footer nav~~ +☑ ~~Compress inline CSS~~ + +*[HTML]: Hyper Text Markup Language
\ No newline at end of file diff --git a/content/_pages/adding-pages.md b/content/_pages/adding-pages.md new file mode 100644 index 0000000..433ffe0 --- /dev/null +++ b/content/_pages/adding-pages.md @@ -0,0 +1,21 @@ +# Adding Custom Pages + +To add your own custom pages, simply create a Markdown file under the `content/_pages` directory. PHPetite will take it from there! + +## Some Caveats + +Any page you create will be automatically added to the `footer` navigation section. If you wish to hide individual pages from showing in the `footer`, do so via CSS: + +```.css +footer a.slug-name-of-your-page { + display: none; +} +``` + +If you want to remove the `footer` navigation altogether, add the following to your `style.css` file: + +```.css +footer .footer-links { + display: none; +} +```
\ No newline at end of file diff --git a/content/_pages/generating-this-blog.md b/content/_pages/generating-this-blog.md new file mode 100644 index 0000000..6e79219 --- /dev/null +++ b/content/_pages/generating-this-blog.md @@ -0,0 +1,32 @@ +# Generating This Blog + +**Important**: Before building and uploading your single file blog, be sure to edit all the proper details found inside the `_phpetite/_config.php` file. This includes your domain, site title, author name, etc. + +Most users won't ever need to fiddle with the other files inside the `_phpetite` directory. + +--- + +Get [PHPetite](https://git.btxx.org/phpetite "PHPetite") in order to convert a collection of Markdown files into a single HTML file with inline CSS. + +1. Make proper edits to the `/_phpetite/_config.php` file +2. Write posts in `/content` +3. (Optional) include any images under the `/content/img/` directory +4. From the command-line run: + +```.shell +make +``` + +This will generate both the single file HTML page, along with an `atom.xml` file for the use of an optional RSS feed. + +These two files are output into the `_site` directory. + +## Looking for more advanced options? + +- [Adding Custom Pages](#adding-pages) +- [Converting from Jekyll](#2021-02-07-converting-from-jekyll) + +*[HTML]: Hyper Text Markup Language +*[CSS]: Cascading Style Sheets +*[URL]: Uniform Resource Locator +*[PHP]: Personal Hypertext Processor
\ No newline at end of file diff --git a/content/_pages/home-content.md b/content/_pages/home-content.md new file mode 100644 index 0000000..b5b8189 --- /dev/null +++ b/content/_pages/home-content.md @@ -0,0 +1,21 @@ +# A Single File Blog + +PHPetite (/p/h/pəˈtēt/) is a single file, static blog generated from PHP. Based off the very minimal and awesome <a target="_blank" href="https://github.com/cadars/portable-php">portable-php</a> + +## Key Features + +- Entire blog is rendered in a single HTML file +- Inline, compressed CSS +- All images converted into base64 encoding +- Minimal requirements / no heavy build tools + +--- + +Feel free to look through the documentation found posted on this site or directly in the github repo. + +## Getting Started + +- [Requirements](#requirements) +- [Generating This Blog](#generating-this-blog) +- [Structuring Blog Posts](#structure) +- [Adding Custom Pages](#adding-pages) diff --git a/content/_pages/requirements.md b/content/_pages/requirements.md new file mode 100644 index 0000000..2abc89a --- /dev/null +++ b/content/_pages/requirements.md @@ -0,0 +1,8 @@ +# Requirements + +1. `PHP 7.3` or higher +2. If using Linux, you will require the following packages in order to convert your images to base64 encoding: + - PHP XML -> `sudo apt-get install php-xml` + - PHP mbstring -> `sudo apt-get install php-mbstring` + +That's really it!
\ No newline at end of file diff --git a/content/_pages/structure.md b/content/_pages/structure.md new file mode 100644 index 0000000..d89761f --- /dev/null +++ b/content/_pages/structure.md @@ -0,0 +1,21 @@ +# Structuring Blog Posts + +Blog posts should be placed into the `/content` directory and be named based only on their post date. See an example here: + +```.markdown +2048-01-01.md +``` + +PHPetite will create a `target` by appending the page title inside the article to the file's date name. So a markdown file with the following content: + +```.markdown +# Bladerunner Rocks + +Bladerunner is amazing because blah blah blah... +``` + +will render out the `target` link as: + +```.markdown +example.com/#2048-01-01-bladerunner-rocks +``` diff --git a/content/img/icon.png b/content/img/icon.png Binary files differnew file mode 100644 index 0000000..98e2a38 --- /dev/null +++ b/content/img/icon.png diff --git a/content/img/image.png b/content/img/image.png Binary files differnew file mode 100644 index 0000000..317aba0 --- /dev/null +++ b/content/img/image.png diff --git a/makefile b/makefile new file mode 100644 index 0000000..597e170 --- /dev/null +++ b/makefile @@ -0,0 +1,8 @@ +.DEFAULT: build + +build: + php _phpetite/phpetite.php > _site/index.html + php _phpetite/rss.php > _site/atom.xml + +serve: build + python3 -m http.server --directory _site/ diff --git a/style.css b/style.css new file mode 100644 index 0000000..e16ea88 --- /dev/null +++ b/style.css @@ -0,0 +1,418 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-kerning: auto; +} + +body { + font: 16px / 1.44 Lucida Grande, system-ui, "Segoe UI", sans-serif; + position: relative; + max-width: 45em; /* column width */ + margin: 0 auto; +} + +pre { + background: rgba(255,255,255,0.1); + border: 1px solid; +} + +/* Show & hide sections */ + +section, section:target ~ section:last-of-type { + height: 0; + overflow: hidden; + padding: 0; +} + +section:target, section:last-of-type { + height: auto; + overflow: visible; + padding: calc(5vw + 2.6em) 4vw 1.6em; +} + +section:focus { + outline: 0; +} + +/* Webkit fix for overflowing text? */ + +section { + overflow-wrap: break-word; + width: 100%; +} + +section .posted-on { + display: block; + font-size: 80%; + margin-bottom: -0.8em; + padding-top: 1em; +} + +/* Vertical spacing */ + +section * + * { + margin-top: .7em; +} + +/* Header */ + +header { + padding: 5vw 4vw 0 4vw; + position: absolute; + width: 100%; + z-index: 1; +} + +/* Footer */ + +footer { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + padding: 1em 4vw 5vw 4vw; +} + +footer .footer-links { + display: block; + width: 100%; +} + +footer .footer-links a { + display: inline-block; +} + +footer .footer-links span.divider { + display: inline-block; + margin: 0 5px; +} +footer .footer-links span.divider:last-of-type { + display: none; +} + +/* Stop the homepage content from appearing as a footer link */ +footer .footer-links a.home-content, footer .footer-links a.home-content + span.divider { + display: none; +} + +footer * + * { + margin-top: 0; +} + +/* Table of contents */ + +ul.toc { + overflow: hidden; +} + +ul.toc * + * { + margin: 0; +} + +ul.toc li { + line-height:1.5; + position: relative; + display: flex; + align-items: flex-end; + margin: 0; +} + +ul.toc li a { + flex: 1; +} + +ul.toc li a span { + padding-right: .3em; +} + +ul.toc li time { + font-variant-numeric: tabular-nums; + padding-left:.3em; + z-index: 1; +} + +ul.toc li:after { + width: 100%; + font-size: .55em; + position: absolute; + bottom: .3em; + white-space: nowrap; + opacity: 0.3; + pointer-events: none; + content: + ' . . . . . . . . . . . . . . . . . . . . . .' + ' . . . . . . . . . . . . . . . . . . . . . .' + ' . . . . . . . . . . . . . . . . . . . . . .' + ' . . . . . . . . . . . . . . . . . . . . . .' + ' . . . . . . . . . . . . . . . . . . . . . .' + ' . . . . . . . . . . . . . . . . . . . . . .'; +} + +/* General */ + +a { + text-decoration: none; + overflow-wrap: break-word; +} + +@media (hover: hover) and (pointer: fine) { + a:hover {text-decoration: underline;} +} + +a[href*="//"]:after { + font-size: .65em; + content: "\2197"; + display: inline-block; +} + +a[href*="//"]:hover:after { + color: inherit; +} + +/* Headings */ + +header h1 { + margin-bottom: 1em; +} +header h1 a { + font-weight: normal; + display: block; +} + +section h1 { + font-size: 1.4em; + line-height:1.5; + padding-top: 0; +} + +header h1, h2, h3, h4, strong, b, dt { + font-size: 1em; +} + +* + h2, * + h3, * + h4 { + margin-top: 1.4em; +} + +h2.cute, h3 { + text-transform: uppercase; + letter-spacing: .06em; + font-size: .9em; + font-weight: 400; +} + +/* Lists */ + +li, dd { + margin-left: 1.5em; +} + +li + li, li ol, li ul { + margin-top: .1em; +} + +.footnotes li { + margin-top:.5em; + max-width:95%; +} + +/* Images */ + +img { + display: block; + max-width: 100%; + min-height:6em; + height: auto; + position: relative; +} + +img:after { /* style offline images */ + align-content:center; + border:1px dashed; + content: attr(alt); + display: grid; + font-size: .8em; + height: 100%; + left: 0; + position: absolute; + text-align:center; + top: 0; + width: 100%; + z-index: 2; +} + +figure { + padding: 1em; +} + +figcaption, small, .footnotes { + font-size: .865em; +} + +/* Other elements */ + +blockquote { + padding: 0 1em; +} + +cite { + font-style: normal; +} + +abbr[title] { + text-decoration: none; + cursor: help; +} + +a abbr[title] { + cursor: pointer; + color: inherit; +} + +hr { + border: 0; + height: 0; + border-bottom: 1px solid; + opacity: .1; + margin: 1.4em 0; +} + +sup { + line-height: 1; + font-size: .75em; + margin-left: .05em; +} + +code, kbd { + font-family: ui-monospace, SF Mono, SFMono-Regular, Menlo, Andale Mono, monospace; + font-size: .9em; + overflow-wrap: break-word; +} + +kbd { + box-shadow:0 .5px 1px; + border-radius:2px; + padding:.05em .325em; + font-size:.85em; + margin: 0 .1em; +} + +pre { + line-height: 1.55; + overflow: auto; + background: rgba(0,0,0,.03); + padding: .5em .85em .6em .85em; + border-radius: 4px; +} + +pre code { + font-size:.9em; + position: relative; + display:block; + overflow-wrap: normal; +} + +pre code:after { + content: attr(class); + position: absolute; + right: -.6em; + top: -.3em; + text-transform: uppercase; + font-size: .7em; + opacity:.45; +} + +input, select, textarea, button { + font: inherit; + font-size: .85em; + line-height: inherit; + border: 0; + box-shadow: 0 0 .05em; + padding: .2em .4em; + width: 100%; +} + +/* Tables */ + +table { + border-collapse: collapse; + min-width: 100%; + margin: 1em 0; +} + +thead { + text-align: left; + border-bottom: 1px solid; +} + +tr + tr { + border-top: 1px solid; +} + +th, td { + padding: .4em .3em .2em; +} + +/* Disable footnotes #links */ + +sup a { + color: currentColor; + pointer-events: none; +} + +a.footnote-backref { + display: none; +} + +/* Print */ + +@media print { + + header { + position: relative; + } + + section { + height: auto; + overflow: visible; + + page-break-after: always; + page-break-inside: avoid; + break-inside: avoid; + display: block; + padding: 2em 4vw; + } + + section * { + page-break-inside: avoid; + break-inside: avoid; + } + +} + +@media only screen and (max-width: 500px) { + footer .footer-links a { display: block; margin: 0.2em 0; } + footer .footer-links span.divider { display: none; } + blockquote, figure { padding-left: 4vw; padding-right: 4vw; } + ul.toc li { + align-items: flex-start; + flex-direction: column-reverse; + } + ul.toc li a { padding-bottom:1em; } + ul.toc li time { font-size: .8em; padding-left: 0; } + ul.toc li a:after { + height: 0; + overflow: hidden; + position: absolute; + } +} + +@supports (color-scheme: dark light) { + @media screen and (prefers-color-scheme: dark) { + a:link {color: #9e9eff;} + a:visited {color: #d0adf0;} + a:active {color: red;} + } +} |