summaryrefslogtreecommitdiff
path: root/includes/util
diff options
context:
space:
mode:
Diffstat (limited to 'includes/util')
-rw-r--r--includes/util/Parsedown.php1994
-rw-r--r--includes/util/agewarning.inc35
-rw-r--r--includes/util/banner.inc480
-rw-r--r--includes/util/bitset.inc21
-rw-r--r--includes/util/functions.inc551
-rw-r--r--includes/util/homepage.inc36
-rw-r--r--includes/util/language.inc3
-rw-r--r--includes/util/profiles.inc97
-rw-r--r--includes/util/pronouns.inc149
-rw-r--r--includes/util/rainbow.inc56
-rw-r--r--includes/util/random.inc15
-rw-r--r--includes/util/score.inc136
-rw-r--r--includes/util/session.inc29
-rw-r--r--includes/util/short.inc55
-rw-r--r--includes/util/travelling.inc62
15 files changed, 3719 insertions, 0 deletions
diff --git a/includes/util/Parsedown.php b/includes/util/Parsedown.php
new file mode 100644
index 0000000..3e29589
--- /dev/null
+++ b/includes/util/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',
+ );
+} \ No newline at end of file
diff --git a/includes/util/agewarning.inc b/includes/util/agewarning.inc
new file mode 100644
index 0000000..1f51647
--- /dev/null
+++ b/includes/util/agewarning.inc
@@ -0,0 +1,35 @@
+<?php
+
+function showWarning($name, $id, $system) {
+ $ntfy = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true)["ntfy"];
+
+ if (time() > 1677628800) {
+ file_get_contents('https://' . $ntfy["server"] . '/' . $ntfy['topic'], false, stream_context_create([
+ 'http' => [
+ 'method' => 'POST',
+ 'header' =>
+ "Content-Type: text/plain\r\n" .
+ "Title: ⚠️ $name does not have an age or birth year set\r\n" .
+ "Priority: max\r\n" .
+ "Tags: switch\r\n" .
+ "Actions: view, Edit on Cold Haze, https://ponies.equestria.horse/-/metadata/" . ($system === "gdapd" ? "raindrops" : "cloudburst") . "/" . $id . "/, clear=true\r\n" .
+ "Authorization: Basic " . base64_encode($ntfy["user"] . ":" . $ntfy["password"]),
+ 'content' => "To make sure they appear on the fronting schedule (and to make sure they can front again), they need to set an age or birth year now."
+ ]
+ ]));
+ } else {
+ file_get_contents('https://' . $ntfy["server"] . '/' . $ntfy['topic'], false, stream_context_create([
+ 'http' => [
+ 'method' => 'POST',
+ 'header' =>
+ "Content-Type: text/plain\r\n" .
+ "Title: ⚠️ $name does not have an age or birth year set\r\n" .
+ "Priority: max\r\n" .
+ "Tags: switch\r\n" .
+ "Actions: view, Edit on Cold Haze, https://ponies.equestria.horse/-/metadata/" . ($system === "gdapd" ? "raindrops" : "cloudburst") . "/" . $id . "/, clear=true\r\n" .
+ "Authorization: Basic " . base64_encode($ntfy["user"] . ":" . $ntfy["password"]),
+ 'content' => "To make sure they still appear on the fronting schedule after March 1st, they need to set an age or birth year now."
+ ]
+ ]));
+ }
+} \ No newline at end of file
diff --git a/includes/util/banner.inc b/includes/util/banner.inc
new file mode 100644
index 0000000..46d9ce5
--- /dev/null
+++ b/includes/util/banner.inc
@@ -0,0 +1,480 @@
+<?php
+
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/session.inc"; global $isLoggedIn; global $lang; global $pages;
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/pronouns.inc";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/functions.inc";
+
+$travelling = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/travelling/travelling.json"), true);
+
+function _header_getMember(string $id, $system) {
+ $members = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$system/members.json"), true);
+ $member = null;
+
+ foreach ($members as $m) {
+ if ($m["id"] === $id) $member = $m;
+ }
+
+ return $member;
+}
+
+function getMemberBannerData(string $id, string $system, bool $french = false) {
+ global $travelling;
+ global $isLoggedIn;
+ global $lang;
+
+ $french = $lang["_french"];
+
+ $member = getSystemMember($system, $id);
+ $metadata = file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/metadata/$id.json") ? parseMetadata(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/metadata/$id.json"), true)) : parseMetadata([
+ "bitset" => 4196352,
+ "regression" => null,
+ "median" => null,
+ "marefriends" => [],
+ "sexfriends" => [],
+ "sisters" => [],
+ "caretakers" => []
+ ]);
+
+ $prefixes = [];
+ foreach ($member['proxy_tags'] as $proxy) {
+ if ($travelling[$member['id']]["travelling"]) {
+ if (isset($travelling[$member['id']]['equestria']) && $travelling[$member['id']]['equestria']) {
+ $prefixes[] = null;
+ } else {
+ $prefixes[] = "+" . $proxy["prefix"] . ($proxy["suffix"] !== "" && $proxy["suffix"] !== null ? "..." . $proxy["suffix"] : "");
+ }
+ } else {
+ $prefixes[] = $proxy["prefix"] . ($proxy["suffix"] !== "" && $proxy["suffix"] !== null ? "..." . $proxy["suffix"] : "");
+ }
+ }
+
+ $lastFronted = null;
+ if (!$metadata["median"]) {
+ $fronters = array_map(function ($item) {
+ return $item["id"];
+ }, json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$system/fronters.json"), true)["members"]);
+
+ if (in_array($id, $fronters)) {
+ $lastFronted = [
+ 'now' => true,
+ 'relative' => timeAgo(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$system/fronters.json"), true)["timestamp"]),
+ 'absolute' => date("D j M Y, G:i:s (e)", strtotime(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$system/fronters.json"), true)["timestamp"])),
+ 'timestamp' => strtotime(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$system/fronters.json"), true)["timestamp"]),
+ 'duration' => [
+ 'seconds' => null,
+ 'pretty' => null
+ ]
+ ];
+ } else {
+ $switches = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$system/switches.json"), true);
+
+ $thisMember = array_filter($switches, function ($item) {
+ global $memberData;
+ return in_array($memberData["id"], $item["members"]);
+ });
+
+ $thisMember = array_values($thisMember);
+ $frontingEnd = null;
+
+ if (count($thisMember) > 0) {
+ $thisIndex = array_search($thisMember[0], $switches);
+
+ $frontingStart = $thisMember[0];
+ $frontingEnd = $switches[$thisIndex - 1];
+ }
+
+ if ($frontingEnd !== null && isset($frontingStart)) {
+ $seconds = (strtotime($frontingEnd["timestamp"]) - strtotime($frontingStart["timestamp"]));
+
+ $lastFronted = [
+ 'now' => false,
+ 'relative' => timeAgo($frontingEnd["timestamp"]),
+ 'absolute' => date("D j M Y, G:i:s (e)", strtotime($frontingEnd["timestamp"])),
+ 'timestamp' => strtotime($frontingEnd["timestamp"]),
+ 'duration' => [
+ 'seconds' => $seconds,
+ 'pretty' => $seconds . ($french ? " secondes" : " seconds")
+ ]
+ ];
+ if ($french) {
+ if ($seconds > 60) {
+ if ($seconds > 3600) {
+ $lastFronted['duration']['pretty'] = round($seconds / 3600) . " heures";
+ } else {
+ $lastFronted['duration']['pretty'] = round($seconds / 60) . " minutes";
+ }
+ }
+ } else {
+ if ($seconds > 60) {
+ if ($seconds > 3600) {
+ $lastFronted['duration']['pretty'] = round($seconds / 3600) . " hours";
+ } else {
+ $lastFronted['duration']['pretty'] = round($seconds / 60) . " minutes";
+ }
+ }
+ }
+ }
+ }
+ }
+
+ $speciesList = [];
+ foreach ($metadata["species"] ?? [] as $species) {
+ if ($french) {
+ $name = match ($species) {
+ "earth" => $metadata["robot"] ? "Robot poney terrestre" : (!$metadata["plush"] ? "Poney terrestre" : "Poney terrestre en peluche"),
+ "alicorn" => $metadata["robot"] ? "Robot alicorne" : (!$metadata["plush"] ? "Alicorne" : "Alicorne en peluche"),
+ "crystal" => $metadata["robot"] ? "Robot poney de crystal" : (!$metadata["plush"] ? "Poney de crystal" : "Poney de crystal en peluche"),
+ "pegasus" => $metadata["robot"] ? "Robot pégase" : (!$metadata["plush"] ? "Pégase" : "Pégase en peluche"),
+ "batpony" => $metadata["robot"] ? "Robot Poney chauve souris" : (!$metadata["plush"] ? "Poney chauve souris" : "Poney chauve souris en peluche"),
+ "unicorn" => $metadata["robot"] ? "Robot licorne" : (!$metadata["plush"] ? "Licorne" : "Licorne en peluche"),
+ "changeling" => "Changelin",
+ default => $species . "_" . $metadata["robot"]
+ };
+ } else {
+ $name = match ($species) {
+ "earth" => $metadata["robot"] ? "Robot earth pony" : (!$metadata["plush"] ? "Earth pony" : "Earth pony plush"),
+ "alicorn" => $metadata["robot"] ? "Robot alicorn" : (!$metadata["plush"] ? "Alicorn" : "Alicorn plush"),
+ "crystal" => $metadata["robot"] ? "Robot crystal pony" : (!$metadata["plush"] ? "Crystal pony" : "Crystal pony plush"),
+ "pegasus" => $metadata["robot"] ? "Robot pegasus" : (!$metadata["plush"] ? "Pegasus" : "Pegasus plush"),
+ "batpony" => $metadata["robot"] ? "Robot bat pony" : (!$metadata["plush"] ? "Bat pony" : "Bat pony plush"),
+ "unicorn" => $metadata["robot"] ? "Robot unicorn" : (!$metadata["plush"] ? "Unicorn" : "Unicorn plush"),
+ "changeling" => "Changeling",
+ default => $species . "_" . $metadata["robot"]
+ };
+ }
+
+ $speciesList[] = [
+ "id" => $species,
+ "name" => $name,
+ "robot" => $metadata["robot"],
+ "plush" => $metadata["plush"],
+ "icon" => $species . ($metadata["robot"] ? "-robot" : ($metadata["plush"] ? "-plush" : "")) . ".png"
+ ];
+ }
+
+ $app = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true);
+
+ $systemData = [];
+ $systemData['page'] = "/" . ($system === "gdapd" ? "raindrops" : ($system === $app["other"]["id"] ? $app["other"]["slug"] : "cloudburst"));
+ $systemData['icon'] = getAsset($system);
+ $systemData['name'] = $system === "gdapd" ? "Raindrops" : ($system === $app["other"]["id"] ? $app["other"]["short"] : "Cloudburst");
+ $systemData['full_name'] = $system === "gdapd" ? "Raindrops System" : ($system === $app["other"]["id"] ? $app["other"]["name"] : "Cloudburst System");
+ $systemData['temporary'] = false;
+
+ if ($travelling[$member['id']]["travelling"] && !$travelling[$member['id']]["equestria"]) {
+ $systemData['page'] = "/" . ($system === "gdapd" ? "cloudburst" : "raindrops");
+ $systemData['icon'] = getAsset($system === "gdapd" ? "ynmuc" : "gdapd");
+ $systemData['name'] = $system === "gdapd" ? "Cloudburst" : "Raindrops";
+ $systemData['full_name'] = $system === "gdapd" ? "Cloudburst System" : "Raindrops System";
+ $systemData['temporary'] = true;
+ }
+
+ if ($travelling[$member['id']]["travelling"] && $travelling[$member['id']]["equestria"]) {
+ $systemData['page'] = null;
+ $systemData['icon'] = "../logo/equestria.png";
+ $systemData['name'] = "Equestria";
+ $systemData['full_name'] = "Equestria";
+ $systemData['temporary'] = true;
+ }
+
+ $marefriends = [];
+ foreach ($metadata["marefriends"] as $marefriend) {
+ $mfSystem = explode("/", $marefriend)[0];
+ $mfMemberID = explode("/", $marefriend)[1];
+ $mfMember = array_filter(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$mfSystem/members.json"), true), function ($item) use ($mfMemberID) {
+ return $item["id"] === $mfMemberID;
+ });
+ sort($mfMember);
+ $mfMember = $mfMember[0];
+
+ $marefriends[] = [
+ "id" => $marefriend,
+ "link" => "/" . ($mfMember["name"]),
+ "icon" => getAsset($mfSystem, $mfMemberID, "heads"),
+ "name" => $mfMember["display_name"] ?? $mfMember["name"],
+ "full_name" => $mfMember["display_name"] ?? $mfMember["name"]
+ ];
+ }
+
+ $sexfriends = null;
+
+ if ($isLoggedIn) {
+ $sexfriends = [];
+ foreach ($metadata["sexfriends"] as $marefriend) {
+ $mfSystem = explode("/", $marefriend)[0];
+ $mfMemberID = explode("/", $marefriend)[1];
+ $mfMember = array_filter(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$mfSystem/members.json"), true), function ($item) use ($mfMemberID) {
+ return $item["id"] === $mfMemberID;
+ });
+ sort($mfMember);
+ $mfMember = $mfMember[0];
+
+ $sexfriends[] = [
+ "id" => $marefriend,
+ "link" => "/" . ($mfMember["name"]),
+ "icon" => getAsset($mfSystem, $mfMemberID, "heads"),
+ "name" => $mfMember["display_name"] ?? $mfMember["name"],
+ "full_name" => $mfMember["display_name"] ?? $mfMember["name"]
+ ];
+ }
+ }
+
+ $sisters = [];
+ foreach ($metadata["sisters"] as $marefriend) {
+ $mfSystem = explode("/", $marefriend)[0];
+ $mfMemberID = explode("/", $marefriend)[1];
+ $mfMember = array_filter(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$mfSystem/members.json"), true), function ($item) use ($mfMemberID) {
+ return $item["id"] === $mfMemberID;
+ });
+ sort($mfMember);
+ $mfMember = $mfMember[0];
+
+ $sisters[] = [
+ "id" => $marefriend,
+ "link" => "/" . ($mfMember["name"]),
+ "icon" => getAsset($mfSystem, $mfMemberID, "heads"),
+ "name" => $mfMember["display_name"] ?? $mfMember["name"],
+ "full_name" => $mfMember["display_name"] ?? $mfMember["name"]
+ ];
+ }
+
+ $caretakers = null;
+
+ if ($metadata["little"] >= 2) {
+ $caretakers = [];
+ foreach ($metadata["caretakers"] as $marefriend) {
+ $mfSystem = explode("/", $marefriend)[0];
+ $mfMemberID = explode("/", $marefriend)[1];
+ $mfMember = array_filter(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$mfSystem/members.json"), true), function ($item) use ($mfMemberID) {
+ return $item["id"] === $mfMemberID;
+ });
+ sort($mfMember);
+ $mfMember = $mfMember[0];
+
+ $caretakers[] = [
+ "id" => $marefriend,
+ "link" => "/" . ($mfMember["name"]),
+ "icon" => getAsset($mfSystem, $mfMemberID, "heads"),
+ "name" => $mfMember["display_name"] ?? $mfMember["name"],
+ "full_name" => $mfMember["display_name"] ?? $mfMember["name"]
+ ];
+ }
+ }
+
+ $badges = [];
+
+ if ($metadata["host"] ?? false) {
+ if (!$travelling[$member['id']]["travelling"]) {
+ $badges[] = [
+ "id" => "mcf",
+ "color" => "primary",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Fronteuse la plus présente</b><br>Elle est le membre qui fronte le plus dans son système." class="badge rounded-pill bg-primary">Fronteuse la plus présente</span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Most common fronter</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' the one who fronts the most often in ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' system." class="badge rounded-pill bg-primary">Most common fronter</span>'
+ )
+ ];
+ }
+ }
+
+ /*if (($metadata["age_spells"] ?? false) && !$french) {
+ $badges[] = [
+ "id" => "age_spells",
+ "color" => "#6f42c1",
+ "html" => '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Affected by age spells</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' can feel younger than ' . getMemberPronouns($member['pronouns'])["subjective"] . ' actually ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' when somepony else casts an age spell on ' . getMemberPronouns($member['pronouns'])["object"] . '." class="badge rounded-pill" style="background-color: #6f42c1;">Affected by age spells</span>'
+ ];
+ }*/
+
+ if ($metadata["fictive"] ?? false) {
+ $badges[] = [
+ "id" => "fictive",
+ "color" => "info",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Fictive</b><br>Elle est basée sur la personnalité, apparence et comportement d\'un personnage qui est fictif dans ce monde." class="badge rounded-pill bg-info">Fictive</span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Fictive</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' based on the personality, look and behavior of a character that is fictional in this world." class="badge rounded-pill bg-info">Fictive</span>'
+ )
+ ];
+ }
+
+ if ($metadata["persecutor"] ?? false) {
+ $badges[] = [
+ "id" => "persecutor",
+ "color" => "danger",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Persécutrice</b><br>Elle se comporte de façon nuisible à l\'égart des autres ou des autres membres du système, potentiellement comme une tentative malavisée de protéger le système." class="badge rounded-pill bg-danger">Persécutrice</span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Persecutor</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "acts" : "act") . ' harmfully towards other system members and/or others, potentially as a misguided attempt to protect the system." class="badge rounded-pill bg-danger">Persecutor</span>'
+ )
+ ];
+ }
+
+ if ($metadata["less_frequent"] ?? false) {
+ $badges[] = [
+ "id" => "nonverbal",
+ "color" => "#6610f2",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Fronts less often</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "front" : "fronts") . ' less often than ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' headmates, this can due to various reasons." class="badge rounded-pill" style="background-color:#6610f2;">Fronte moins souvent</span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Fronts less often</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "front" : "fronts") . ' less often than ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' headmates, this can due to various reasons." class="badge rounded-pill" style="background-color:#6610f2;">Fronts less often</span>'
+ )
+ ];
+ }
+
+ if ($metadata["nonverbal"] ?? false) {
+ $badges[] = [
+ "id" => "nonverbal",
+ "color" => "#20c997",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Non verbale dans la réalité</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' non verbal in real life, although text communication is still possible." class="badge rounded-pill" style="background-color:#20c997;">Non verbal IRL</span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Non verbal IRL</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' non verbal in real life, although text communication is still possible." class="badge rounded-pill" style="background-color:#20c997;">Non verbal IRL</span>'
+ )
+ ];
+ }
+
+ if ($member["name"] === "fusion") {
+ $badges[] = [
+ "id" => "fusion",
+ "color" => "d63384",
+ "html" => '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Fusion</b><br>She is the result of multiple ponies purposefully merging in a way that makes them impossible to tell apart." class="badge rounded-pill" style="background-color:#d63384;">Fusion</span>'
+ ];
+ }
+
+ if (($metadata["leader"] ?? false)) {
+ $badges[] = [
+ "id" => "leader",
+ "color" => "d6a833",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Leader</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' a leader in ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' system." class="badge rounded-pill" style="background-color:#fd7e14;">Dirigeante</span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Leader</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' a leader in ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' system." class="badge rounded-pill" style="background-color:#fd7e14;">Leader</span>'
+ )
+ ];
+ }
+
+ if ($metadata["protector"] ?? false) {
+ $badges[] = [
+ "id" => "protector",
+ "color" => "black",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>' . (getMemberPronouns($member['pronouns'])["gender"] === "female" ? "Protectrice" : (getMemberPronouns($member['pronouns'])["gender"] === "male" ? "Protecteur" : "Protectrice")) . '</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' a protector in the system and will front when somepony cannot handle it anymore." class="badge rounded-pill bg-black">' . (getMemberPronouns($member['pronouns'])["gender"] === "female" ? "Protectrice" : (getMemberPronouns($member['pronouns'])["gender"] === "male" ? "Protecteur" : "Protectrice")) . '</span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Protector</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' a protector in the system and will front when somepony cannot handle it anymore." class="badge rounded-pill bg-black">Protector</span>'
+ )
+ ];
+ }
+
+ if (($metadata["little"] ?? 0) === 2) {
+ $badges[] = [
+ "id" => "little",
+ "color" => "success",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Petit' . (getMemberPronouns($member['pronouns'])["gender"] === "female" ? "e" : (getMemberPronouns($member['pronouns'])["gender"] === "male" ? "" : "·e")) . '</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' mentally younger, and therefore behaves and feels younger than the body is." class="badge rounded-pill bg-success">Petit' . (getMemberPronouns($member['pronouns'])["gender"] === "female" ? "e" : (getMemberPronouns($member['pronouns'])["gender"] === "male" ? "" : "·e")) . '</span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Little</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' mentally younger, and therefore behaves and feels younger than the body is." class="badge rounded-pill bg-success">Little</span>'
+ )
+ ];
+ }
+
+ if (($metadata["little"] ?? 0) === 3) {
+ $badges[] = [
+ "id" => "younger",
+ "color" => "dark",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Plus jeune</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' younger than the body, but not young enough to be classified as a little." class="badge rounded-pill bg-dark">Plus jeune</span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Younger</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' younger than the body, but not young enough to be classified as a little." class="badge rounded-pill bg-dark">Younger</span>'
+ )
+ ];
+ }
+
+ if (($metadata["age_regressor"] ?? false)) {
+ if ($metadata["regression"] !== null && $metadata["regression"] !== false) {
+ $regression = _header_getMember($metadata["regression"], $system);
+ $badges[] = [
+ "id" => "regressor_median",
+ "color" => "secondary",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Régresseur d\'âge</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' capable of regressing ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' mental age, which causes ' . getMemberPronouns($member['pronouns'])["object"] . ' to become ' . (getMiniName($regression["display_name"] ?? $regression["name"])) . ', temporarily behaving and feeling younger than the body is." class="badge rounded-pill bg-secondary">Régresse en âge en <a href="/' . $regression["name"] . '">'. (getMiniName($regression["display_name"] ?? $regression["name"])) . '</a></span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Age regressor</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' capable of regressing ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' mental age, which causes ' . getMemberPronouns($member['pronouns'])["object"] . ' to become ' . (getMiniName($regression["display_name"] ?? $regression["name"])) . ', temporarily behaving and feeling younger than the body is." class="badge rounded-pill bg-secondary">Age regresses into <a href="/' . $regression["name"] . '">'. (getMiniName($regression["display_name"] ?? $regression["name"])) . '</a></span>'
+ )
+ ];
+ }
+ }
+
+ if ($metadata["median"] !== null && $metadata["median"] !== false) {
+ $source = _header_getMember($metadata["median"], $system);
+ if ($metadata["little"] > 0) {
+ $badges[] = [
+ "id" => "regressed",
+ "color" => "warning",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Régressée en âge</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' has regressed ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' mental age, making ' . getMemberPronouns($member['pronouns'])["object"] . ' ' . getMiniName($member["display_name"] ?? $member["name"]) . ' instead of ' . (getMiniName($source["display_name"] ?? $source["name"])) . ', temporarily behaving and feeling younger than the body is." class="badge rounded-pill bg-warning">Régressé en âge de <a href="/' . $source["name"] . '">' . (getMiniName($source["display_name"] ?? $source["name"])) . '</a></span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Age regressed</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' has regressed ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' mental age, making ' . getMemberPronouns($member['pronouns'])["object"] . ' ' . getMiniName($member["display_name"] ?? $member["name"]) . ' instead of ' . (getMiniName($source["display_name"] ?? $source["name"])) . ', temporarily behaving and feeling younger than the body is." class="badge rounded-pill bg-warning">Age regressed from <a href="/' . $source["name"] . '">' . (getMiniName($source["display_name"] ?? $source["name"])) . '</a></span>'
+ )
+ ];
+ } else {
+ $badges[] = [
+ "id" => "facet",
+ "color" => "light",
+ "html" => (
+ $french
+ ?
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Facette</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' is a facet of ' . getMiniName($source["display_name"] ?? $source["name"]) . ', meaning ' . getMemberPronouns($member['pronouns'])["subjective"] . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' not totally independent of ' . getMiniName($source["display_name"] ?? $source["name"]) . '." class="badge rounded-pill bg-light">Facette de <a href="/' . $source["name"] . '">' . getMiniName($source["display_name"] ?? $source["name"]) . '</a></span>'
+ :
+ '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Facet</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' is a facet of ' . getMiniName($source["display_name"] ?? $source["name"]) . ', meaning ' . getMemberPronouns($member['pronouns'])["subjective"] . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' not totally independent of ' . getMiniName($source["display_name"] ?? $source["name"]) . '." class="badge rounded-pill bg-light">Facet of <a href="/' . $source["name"] . '">' . getMiniName($source["display_name"] ?? $source["name"]) . '</a></span>'
+ )
+ ];
+ }
+ }
+
+ return [
+ 'id' => $member['name'],
+ 'color' => $member["color"] ?? "000000",
+ 'icon' => [
+ "online" => getAsset($system, $member["id"]),
+ "offline" => "pf-$system-$id.webp"
+ ],
+ 'median' => $metadata["median"],
+ 'little' => $metadata["little"] >= 2,
+ 'name' => $member["display_name"] ?? $member["name"],
+ 'badges' => $badges,
+ 'prefixes' => $prefixes,
+ 'pronouns' => getTooltipsFromMark($member["pronouns"], $french) ?? "<span data-bs-toggle='tooltip' title='Pronouns not specified' class='text-muted'>they/them</span>",
+ 'pronouns_usage' => getMemberPronouns($member['pronouns']),
+ 'last_fronted' => $lastFronted,
+ 'species' => $speciesList,
+ 'system' => $systemData,
+ 'relations' => [
+ 'marefriends' => $marefriends ?? [],
+ 'sexfriends' => $sexfriends ?? null,
+ 'sisters' => $sisters ?? [],
+ 'caretakers' => $caretakers ?? []
+ ]
+ ];
+} \ No newline at end of file
diff --git a/includes/util/bitset.inc b/includes/util/bitset.inc
new file mode 100644
index 0000000..131cb39
--- /dev/null
+++ b/includes/util/bitset.inc
@@ -0,0 +1,21 @@
+<?php
+
+function parseMetadata ($metadata) {
+ $metadata["little"] = 0;
+
+ $age = -1;
+
+ if (isset($metadata["birth"]["age"]) && $metadata["birth"]["age"] !== 0) {
+ $age = $metadata["birth"]["age"];
+ } else if (isset($metadata["birth"]["year"]) && $metadata["birth"]["year"] > 1990) {
+ $age = (int)date('Y') - $metadata["birth"]["year"] + (strtotime(date('Y') . "-" . $metadata["birth"]["date"]) <= time() ? 0 : -1);
+ }
+
+ if ($age > 0 && $age <= 10) {
+ $metadata["little"] = 2;
+ } else if ($age > 0 && $age <= 15) {
+ $metadata["little"] = 3;
+ }
+
+ return $metadata;
+} \ No newline at end of file
diff --git a/includes/util/functions.inc b/includes/util/functions.inc
new file mode 100644
index 0000000..24b059a
--- /dev/null
+++ b/includes/util/functions.inc
@@ -0,0 +1,551 @@
+<?php
+
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/score.inc";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/bitset.inc";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/homepage.inc";
+
+if (!function_exists("peh_error")) {
+ function peh_error($message, $code = 500): void {
+ header("Location: /?em=" . urlencode(base64_encode($message)) . "&ec=" . $code);
+ die();
+ }
+}
+
+if (!function_exists("getAsset")) {
+ function getAsset($systemID, $memberID = null, $type = "avatars") {
+ $app = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true);
+ $systemFile = (isset($app["other"]) && $systemID === $app["other"]["id"]) ? "other" : $systemID;
+
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$systemFile/general.json")) {
+ $id1 = preg_replace("/^([\da-f]{8})-([\da-f]{4})-([\da-f]{4})-([\da-f]{4})-([\da-f]{12})$/", "$1$2$3$4$5", json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$systemFile/general.json"), true)["uuid"]);
+
+ if (isset($memberID)) {
+ $members = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$systemFile/members.json"), true);
+ $list = array_map(function ($i) {
+ return $i["id"];
+ }, $members);
+
+ if (in_array($memberID, $list)) {
+ $id2 = preg_replace("/^([\da-f]{8})-([\da-f]{4})-([\da-f]{4})-([\da-f]{4})-([\da-f]{12})$/", "$1$2$3$4$5", (getMemberWithoutSystem($memberID) ?? ['uuid' => ''])["uuid"]);
+ } else {
+ return "/error/nomember/?s=$systemID&m=$memberID&t=$type";
+ }
+
+ $id = $id1 . $id2;
+
+ if (str_ends_with((getMemberWithoutSystem($memberID) ?? ['name' => ''])["name"], "-travelling")) {
+ $id1 = preg_replace("/^([\da-f]{8})-([\da-f]{4})-([\da-f]{4})-([\da-f]{4})-([\da-f]{12})$/", "$1$2$3$4$5", json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/" . ($systemID === "gdapd" ? "ynmuc" : "gdapd") . "/general.json"), true)["uuid"]);
+
+ $members = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/" . ($systemID === "gdapd" ? "ynmuc" : "gdapd") . "/members.json"), true);
+ $list = array_map(function ($i) {
+ return $i["name"];
+ }, $members);
+
+ if (in_array(substr(getMemberWithoutSystem($memberID)["name"], 0, -11), $list)) {
+ $id2 = preg_replace("/^([\da-f]{8})-([\da-f]{4})-([\da-f]{4})-([\da-f]{4})-([\da-f]{12})$/", "$1$2$3$4$5", getMemberFromName(substr(getMemberWithoutSystem($memberID)["name"], 0, -11))["uuid"]);
+ } else {
+ return "/error/nomember/?s=$systemID&m=$memberID&t=$type";
+ }
+
+ $id = $id1 . $id2;
+ }
+ } else {
+ $id = $id1;
+ }
+
+ if ($type === "bodies" || $type === "heads") {
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/" . $type . "/" . $id . ".png")) {
+ return "/assets/" . $type . "/" . $id . ".png";
+ } else {
+ return "/error/nofile/?s=$systemID&m=$memberID&t=$type";
+ }
+ } else {
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/" . $type . "/" . $id . ".webp")) {
+ return "/assets/" . $type . "/" . $id . ".webp";
+ } else {
+ return "/error/nofile/?s=$systemID&m=$memberID&t=$type";
+ }
+ }
+ } else {
+ return "/error/nosys/?s=$systemID&m=$memberID&t=$type";
+ }
+ }
+}
+
+if (!function_exists("rgbToHsl")) {
+ function rgbToHsl($r, $g, $b) {
+ $oldR = $r;
+ $oldG = $g;
+ $oldB = $b;
+
+ $r /= 255;
+ $g /= 255;
+ $b /= 255;
+
+ $max = max($r, $g, $b);
+ $min = min($r, $g, $b);
+
+ $l = ($max + $min) / 2;
+ $d = $max - $min;
+
+ if ($d == 0) {
+ $h = $s = 0;
+ } else {
+ $s = $d / (1 - abs(2 * $l - 1));
+
+ switch ($max) {
+ case $r:
+ $h = 60 * fmod((($g - $b) / $d), 6);
+ if ($b > $g) {
+ $h += 360;
+ }
+ break;
+
+ case $g:
+ $h = 60 * (($b - $r) / $d + 2);
+ break;
+
+ case $b:
+ $h = 60 * (($r - $g) / $d + 4);
+ break;
+ }
+ }
+
+ return array(round($h, 2), round($s, 2), round($l, 2));
+ }
+}
+
+if (!function_exists("imageCreateCorners")) {
+ function imageCreateCorners($sourceImageFile, $radius) {
+ # test source image
+ if (file_exists($sourceImageFile)) {
+ $res = is_array($info = getimagesize($sourceImageFile));
+ }
+ else $res = false;
+
+ # open image
+ if ($res) {
+ $w = $info[0];
+ $h = $info[1];
+ switch ($info['mime']) {
+ case 'image/jpeg': $src = imagecreatefromjpeg($sourceImageFile);
+ break;
+ case 'image/gif': $src = imagecreatefromgif($sourceImageFile);
+ break;
+ case 'image/png': $src = imagecreatefrompng($sourceImageFile);
+ break;
+ default:
+ $res = false;
+ }
+ }
+
+ # create corners
+ if ($res) {
+
+ $q = 10; # change this if you want
+ $radius *= $q;
+
+ # find unique color
+ do {
+ $r = rand(0, 255);
+ $g = rand(0, 255);
+ $b = rand(0, 255);
+ }
+ while (imagecolorexact($src, $r, $g, $b) < 0);
+
+ $nw = $w*$q;
+ $nh = $h*$q;
+
+ $img = imagecreatetruecolor($nw, $nh);
+ $alphacolor = imagecolorallocatealpha($img, $r, $g, $b, 127);
+ imagealphablending($img, false);
+ imagesavealpha($img, true);
+ imagefilledrectangle($img, 0, 0, $nw, $nh, $alphacolor);
+
+ imagefill($img, 0, 0, $alphacolor);
+ imagecopyresampled($img, $src, 0, 0, 0, 0, $nw, $nh, $w, $h);
+
+ imagearc($img, $radius-1, $radius-1, $radius*2, $radius*2, 180, 270, $alphacolor);
+ imagefilltoborder($img, 0, 0, $alphacolor, $alphacolor);
+ imagearc($img, $nw-$radius, $radius-1, $radius*2, $radius*2, 270, 0, $alphacolor);
+ imagefilltoborder($img, $nw-1, 0, $alphacolor, $alphacolor);
+ imagearc($img, $radius-1, $nh-$radius, $radius*2, $radius*2, 90, 180, $alphacolor);
+ imagefilltoborder($img, 0, $nh-1, $alphacolor, $alphacolor);
+ imagearc($img, $nw-$radius, $nh-$radius, $radius*2, $radius*2, 0, 90, $alphacolor);
+ imagefilltoborder($img, $nw-1, $nh-1, $alphacolor, $alphacolor);
+ imagealphablending($img, true);
+ imagecolortransparent($img, $alphacolor);
+
+ # resize image down
+ $dest = imagecreatetruecolor($w, $h);
+ imagealphablending($dest, false);
+ imagesavealpha($dest, true);
+ imagefilledrectangle($dest, 0, 0, $w, $h, $alphacolor);
+ imagecopyresampled($dest, $img, 0, 0, 0, 0, $w, $h, $nw, $nh);
+
+ # output image
+ $res = $dest;
+ imagedestroy($src);
+ imagedestroy($img);
+ }
+
+ return $res;
+ }
+}
+
+if (!function_exists("getMiniName")) {
+ function getMiniName(string $name) {
+ $parts = explode(" ", $name);
+
+ if (strlen($parts[0]) > 3 && $parts[0] !== "Sweetie" && $parts[0] !== "Filly" && $parts[0] !== "Windy" && (isset($parts[1]) && $parts[1] !== "Brightdawn" && $parts[1] !== "Fizz")) {
+ if ($parts[0] === "Princess") {
+ array_shift($parts);
+ }
+
+ if (str_contains($parts[0], "/")) {
+ return explode("/", $parts[0])[0];
+ } else {
+ return $parts[0];
+ }
+ } else {
+ return $name;
+ }
+ }
+}
+
+if (!function_exists("withCaretakersDown")) {
+ function withCaretakersDown(array $ordered): array {
+ return $ordered;
+ }
+}
+
+if (!function_exists("getSystemMember")) {
+ function getSystemMember(string $system, string $id) {
+ $app = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true);
+ $systemID = $system;
+
+ $members = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/" . ($systemID === $app["other"]["id"] ? "other" : $systemID) . "/members.json"), true);
+ $member = null;
+
+ foreach ($members as $m) {
+ if ($m["id"] === $id) $member = $m;
+ }
+
+ $member["system"] = $member["_system"] = $system;
+
+ return $member;
+ }
+}
+
+if (!function_exists("getMemberWithoutSystem")) {
+ function getMemberWithoutSystem(string $id) {
+ global $isLowerLoggedIn; global $isLoggedIn;
+ $member = null;
+
+ $members1 = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc/members.json"), true);
+ foreach ($members1 as $m) {
+ $m["_system"] = "ynmuc";
+ $m["system"] = "ynmuc";
+ if ($m["id"] === $id) $member = $m;
+ }
+
+ $members2 = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd/members.json"), true);
+ foreach ($members2 as $m) {
+ $m["_system"] = "gdapd";
+ $m["system"] = "gdapd";
+ if ($m["id"] === $id) $member = $m;
+ }
+
+ if ($isLowerLoggedIn || $isLoggedIn) {
+ $app = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true);
+ $members3 = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/other/members.json"), true);
+
+ foreach ($members3 as $m) {
+ $m["_system"] = $app["other"]["id"];
+ $m["system"] = $app["other"]["id"];
+ if ($m["id"] === $id) $member = $m;
+ }
+ }
+
+ return $member;
+ }
+}
+
+if (!function_exists("showMembersFromList")) {
+ function showMembersFromList(array $list) {
+ foreach ($list as $member) { if ($member['name'] !== "unknown" && $member['name'] !== "fusion") {
+ echo('<a href="/' . $member['name'] . '" style="text-decoration:none !important;filter:none !important;"><div class="hpd-item-card" style="background-color:rgba(255, 255, 255, .1);border:1px solid ' . (isset($member['color']) ? "#" . $member['color'] . "55" : "transparent") . ';outline-color:' . (isset($member['color']) ? "#" . $member['color'] . "55" : "transparent") . ';border-radius:10px;text-align:center;display:flex;align-items:center;justify-content:center;padding:5px;' . (isset($member["equestria"]) && $member["equestria"] ? 'opacity:.5;' : '') . '"><div>
+<img alt="" src="' . getAsset($member["system"], $member["id"]) . '" style="border-radius:999px;background-color:rgba(0, 0, 0, .25);height:48px;width:48px;display:block;margin-left:auto;margin-right:auto;">
+<div style="text-decoration:none;color:white;margin-top:5px;">' . ($member['display_name'] ?? $member['name']) . '</div>
+<div style="text-decoration:none !important;color:black !important;"><code style="text-decoration:none !important;color:white !important;">' . (isset($member['travelling']) && $member['travelling'] ? "+" . ($member['proxy_tags'][0]['prefix'] ?? "&nbsp;") : ($member['proxy_tags'][0]['prefix'] ?? "&nbsp;")) . '</code></div>
+</div></div></a>');
+ }}
+ }
+}
+
+if (!function_exists("prettySize")) {
+ function prettySize($bytes) {
+ if ($bytes > 1024) {
+ if ($bytes > 1024**2) {
+ if ($bytes > 1024**3) {
+ return round($bytes / 1024**3, 1) . " GB";
+ } else {
+ return round($bytes / 1024**2, 1) . " MB";
+ }
+ } else {
+ return round($bytes / 1024, 1) . " KB";
+ }
+ } else {
+ return $bytes . " B";
+ }
+ }
+}
+
+if (!function_exists("showSystem")) {
+ function showSystem(string $id, string $name, string $color, bool $hideTitle) {
+ $app = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true);
+ global $travelling;
+
+ $global = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/" . (isset($app["other"]) && $id === $app["other"]["id"] ? "other" : $id) . "/general.json"), true);
+
+ if ($hideTitle) {
+ echo('<div id="hpd-' . ($id === "gdapd" ? "raindrops" : ($id === "ynmuc" ? "cloudburst" : "other")) . '" style="background:rgba(255, 255, 255, .1);border-radius:10px;padding:10px;display:grid;grid-template-columns: 1fr;margin-bottom:10px;">');
+ } else {
+ echo('<div id="hpd-' . ($id === "gdapd" ? "raindrops" : ($id === "ynmuc" ? "cloudburst" : "other")) . '" style="background:rgba(255, 255, 255, .1);border-radius:10px;padding:10px 10px 10px 20px;display:grid;grid-template-columns: 128px 1fr;margin-bottom:10px;">');
+ }
+ if (!$hideTitle) echo('<a style="display:flex;margin: -10px -20px;align-items:center;justify-content:center;text-align:center;padding: 10px 20px;border-radius: 10px;background: #' . $global['color'] . '55;width: 148px;text-decoration:none;color:white;filter:none !important;" href="/' . ($id === "gdapd" ? "raindrops" : ($id === $app["other"]["id"] ? $app["other"]["slug"] : "cloudburst")) . '" class="hpd-system">
+<div style="text-align:center;"><img src="' . getAsset($id) . '" style="width:64px;"><br>' . $name . '</div>
+</a>');
+
+ if ($hideTitle) {
+ echo(' <div style="display:grid;grid-template-columns:repeat(6, 1fr);grid-gap:10px;">');
+ } else {
+ echo(' <div style="display:grid;grid-template-columns:repeat(6, 1fr);padding-left:10px;grid-gap:10px;">');
+ }
+
+ if ($id === $app["other"]["id"]) {
+ showMembersFromList(scoreOrder([...array_map(function ($i) use ($id, $travelling) {
+ $i["travelling"] = false;
+ $i["system"] = $id;
+ $i["equestria"] = false;
+ return $i;
+ }, json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/" . (isset($app["other"]) && $id === $app["other"]["id"] ? "other" : $id) . "/members.json"), true))], $id));
+ } else {
+ showMembersFromList(scoreOrder([...array_map(function ($i) use ($id, $travelling) {
+ $i["travelling"] = false;
+ $i["system"] = $id;
+ $i["equestria"] = $travelling[$i['id']]['travelling'] && $travelling[$i['id']]['equestria'];
+ return $i;
+ }, array_filter(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/" . (isset($app["other"]) && $id === $app["other"]["id"] ? "other" : $id) . "/members.json"), true), function ($i) use ($travelling) {
+ return !(isset($travelling[$i['id']]) && $travelling[$i['id']]['travelling'] && (!isset($travelling[$i['id']]['equestria']) || !$travelling[$i['id']]['equestria']));
+ })), ...array_map(function ($i) use ($id) {
+ $i["travelling"] = true;
+ $i["system"] = ($id === "gdapd" ? "ynmuc" : "gdapd");
+ return $i;
+ }, array_filter(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/" . ($id === "gdapd" ? "ynmuc" : "gdapd") . "/members.json"), true), function ($i) use ($travelling) {
+ return isset($travelling[$i['id']]) && $travelling[$i['id']]['travelling'] && (!isset($travelling[$i['id']]['equestria']) || !$travelling[$i['id']]['equestria']);
+ }))], $id));
+ }
+
+ echo('</div>
+
+</div>');
+ }
+}
+
+if (!function_exists("cloudburst")) {
+ function cloudburst(bool $hideTitle): void {
+ showSystem("ynmuc", "Cloudburst System", "#5f08a9a6", $hideTitle);
+ }
+}
+
+
+if (!function_exists("raindrops")) {
+ function raindrops(bool $hideTitle): void {
+ showSystem("gdapd", "Raindrops System", "#a95f08a6", $hideTitle);
+ }
+}
+
+if (!function_exists("other")) {
+ function other(bool $hideTitle): void {
+ $app = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true);
+ showSystem($app["other"]["id"], $app["other"]["name"], "#" . $app["other"]["color"] . "a6", $hideTitle);
+ }
+}
+
+if (!function_exists("getMember")) {
+ function getMember(string $id) {
+ global $systemID;
+
+ $members = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$systemID/members.json"), true);
+ $member = null;
+
+ foreach ($members as $m) {
+ if ($m["id"] === $id) $member = $m;
+ }
+
+ return $member;
+ }
+}
+
+if (!function_exists("timeAgo")) {
+ function timeAgo($time, $french = false): string {
+ if (!is_numeric($time)) {
+ $time = strtotime($time);
+ }
+
+ $periods = ["sec", "min", "hr", "d", "wk", "mo", "y", "ages"];
+ $periods_fr = $periods;
+ $lengths = array("60", "60", "24", "7", "4.35", "12", "100");
+
+ $now = time();
+
+ $difference = $now - $time;
+ if ($difference <= 10 && $difference >= 0) {
+ return $tense = "now";
+ } elseif ($difference > 0) {
+ $tense = "ago";
+ } else {
+ $tense = "later";
+ }
+
+ for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths)-1; $j++) {
+ $difference /= $lengths[$j];
+ }
+
+ $difference = round($difference);
+
+ $period = $periods[$j];
+ return "{$difference} {$period} {$tense}";
+ }
+}
+
+if (!function_exists("timeIn")) {
+ function timeIn($time): string {
+ if (!is_numeric($time)) {
+ $time = strtotime($time);
+ }
+
+ $periods = ["second", "minute", "hour", "day", "week", "month", "year", "age"];
+ $lengths = array("60", "60", "24", "7", "4.35", "12", "100");
+
+ $now = time();
+
+ $difference = $time - $now;
+ if ($difference <= 10 && $difference >= 0) {
+ return $tense = "now";
+ } elseif ($difference > 0) {
+ $tense = "in";
+ } else {
+ $tense = "ago";
+ }
+
+ for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths)-1; $j++) {
+ $difference /= $lengths[$j];
+ }
+
+ $difference = round($difference);
+
+ $period = $periods[$j] . ($difference >1 ? "s" :'');
+ return "{$tense} {$difference} {$period}";
+ }
+}
+
+if (!function_exists("duration")) {
+ function duration($seconds) {
+ global $lang; global $pages;
+
+ if ($seconds >= 60) {
+ if (floor($seconds / 60) >= 60) {
+ if (floor($seconds / 3600) >= 24) {
+ $days = floor($seconds / 86400);
+
+ if ($lang["_french"]) {
+ return $days . " jour" . ($days > 1 ? "s" : "");
+ } else {
+ return $days . " day" . ($days > 1 ? "s" : "");
+ }
+ } else {
+ $hours = floor($seconds / 3600);
+
+ if ($lang["_french"]) {
+ return $hours . " heure" . ($hours > 1 ? "s" : "");
+ } else {
+ return $hours . " hour" . ($hours > 1 ? "s" : "");
+ }
+ }
+ } else {
+ $minutes = floor($seconds / 60);
+ return $minutes . " minute" . ($minutes > 1 ? "s" : "");
+ }
+ } else {
+ if ($lang["_french"]) {
+ return $seconds . " secondes";
+ } else {
+ return $seconds . " seconds";
+ }
+ }
+ }
+}
+
+if (!function_exists("relativeDate")) {
+ function relativeDate($date, $showTime = true) {
+ if (!is_numeric($date)) $date = strtotime($date);
+
+ if (!$showTime) {
+ if (date('Y-m-d', $date) === date('Y-m-d')) {
+ return "today";
+ } elseif (date('Y-m-d', $date) === date('Y-m-d', time() + 86400)) {
+ return "tomorrow";
+ } elseif ($date < time() + 518400) {
+ return date('l', $date);
+ } else {
+ return date('D j M', $date);
+ }
+ } else {
+ if (date('Y-m-d', $date) === date('Y-m-d')) {
+ return "today, <span class='time-adjust'>" . date('H:i', $date) . "</span>";
+ } elseif (date('Y-m-d', $date) === date('Y-m-d', time() + 86400)) {
+ return "tomorrow, <span class='time-adjust'>" . date('H:i', $date) . "</span>";
+ } elseif ($date < time() + 518400) {
+ return date('l', $date) . ", <span class='time-adjust'>" . date('H:i', $date) . "</span>";
+ } else {
+ return date('D j M', $date) . ", <span class='time-adjust'>" . date('H:i', $date) . "</span>";
+ }
+ }
+ }
+}
+
+if (!function_exists("getMemberSystem")) {
+ function getMemberSystem(string $id) {
+ $list = scoreOrderGlobal();
+
+ foreach ($list as $item) {
+ if ($item["id"] === $id) return $item["_system"];
+ }
+ }
+}
+
+if (!function_exists("getMemberFromName")) {
+ function getMemberFromName(string $name) {
+ $list = [...json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd/members.json"), true), ...json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc/members.json"), true), ...json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/other/members.json"), true)];
+
+ foreach ($list as $item) {
+ if ($item["name"] === $name) return getMemberWithoutSystem($item["id"]);
+ }
+ }
+}
+
+if (!function_exists("resolveMember")) {
+ function resolveMember(mixed $name) {
+ if (is_string($name)) {
+ if (str_ends_with($name, "-travelling")) {
+ return substr($name, 0, strlen($name) - 11);
+ } else {
+ return $name;
+ }
+ } else {
+ return $name;
+ }
+ }
+} \ No newline at end of file
diff --git a/includes/util/homepage.inc b/includes/util/homepage.inc
new file mode 100644
index 0000000..ebb99da
--- /dev/null
+++ b/includes/util/homepage.inc
@@ -0,0 +1,36 @@
+<?php
+
+global $app;
+global $travelling;
+
+function newHomepage($id, $page, $title) {
+ global $travelling;
+ $system = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/" . $id . "/general.json"), true);
+
+ ?>
+ <div class="new-homepage-system" id="new-homepage-system-<?= $id ?>">
+ <?php if ($title): ?>
+ <a href="/<?= $page ?>" style="height: 48px; display: flex; align-items: center; justify-content: center; border-top-left-radius: 10px; border-top-right-radius: 10px; text-align: center; text-decoration: none; color: white; background-color: #<?= $system["color"] ?? "000000" ?>55" class="new-homepage-system-title" id="new-homepage-system-<?= $id ?>-title">
+ <div>
+ <img src="<?= getAsset($id) ?>" style="border-radius: 5px;width: 48px; vertical-align: middle;"><b style="vertical-align: middle; margin-left: 5px;"><?= $system["name"] ?></b>
+ </div>
+ </a>
+ <?php endif; ?>
+
+ <div class="new-homepage-system-list" id="new-homepage-system-<?= $id ?>-list">
+ <?php $list = array_values(scoreOrder(withTravelers(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/" . $id . "/members.json"), true), $id), $id)); foreach ($list as $index => $member): ?>
+ <a href="/<?= $member["name"] ?>" class="new-homepage-link <?= $index === count($list) - 1 ? "new-homepage-link-last" : "" ?>" style="color: white !important; text-decoration: none !important; display: block; background-color: #<?= $member["color"] ?>22; padding: 5px 10px;">
+ <img src="<?= getAsset(($travelling[$member["id"]]["travelling"] && !$travelling[$member["id"]]["equestria"]) ? ($id === "gdapd" ? "ynmuc" : "gdapd") : $id, $member["id"]) ?>" style="width: 32px; height: 32px; vertical-align: middle; background-color: rgba(0, 0, 0, .25); border-radius: 999px;">
+ <span style="width: calc(100% - 37px); vertical-align: middle; margin-left: 5px;text-overflow: ellipsis;white-space: nowrap;overflow: hidden;"><?= $member["display_name"] ?? $member["name"] ?></span>
+ <span style="vertical-align: middle; float:right;">
+ <?php if ($travelling[$member["id"]]["travelling"] && $travelling[$member["id"]]["equestria"]): ?>
+ <img src="/assets/logo/equestria.png" style="width: 24px; height: 24px; margin-top: 5px;">
+ <?php elseif ($travelling[$member["id"]]["travelling"]): ?>
+ <img src="<?= getAsset($id === "gdapd" ? "ynmuc" : "gdapd") ?>" style="width: 24px; height: 24px; margin-top: 5px;">
+ <?php endif; ?>
+ </span>
+ </a>
+ <?php endforeach; ?>
+ </div>
+ </div>
+<?php } \ No newline at end of file
diff --git a/includes/util/language.inc b/includes/util/language.inc
new file mode 100644
index 0000000..e24255f
--- /dev/null
+++ b/includes/util/language.inc
@@ -0,0 +1,3 @@
+<?php
+
+$lang = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/i18n/en.json"), true); \ No newline at end of file
diff --git a/includes/util/profiles.inc b/includes/util/profiles.inc
new file mode 100644
index 0000000..66274c7
--- /dev/null
+++ b/includes/util/profiles.inc
@@ -0,0 +1,97 @@
+<?php
+
+function calculateProfileScore($member) {
+ $values = [
+ ((isset($member["_metadata"]["birth"]["age"]) && $member["_metadata"]["birth"]["age"] > 0) || isset($member["_metadata"]["birth"]["year"]) && $member["_metadata"]["birth"]["year"] > 1900) || ((isset($member["_metadata"]["birth"]["age"]) && $member["_metadata"]["birth"]["age"] === -1) && in_array("alicorn", $member["_metadata"]["species"])),
+ isset($member["_metadata"]["birth"]["date"]) && trim($member["_metadata"]["birth"]["date"]) !== "" && $member["_metadata"]["birth"]["date"] !== "01-01",
+ file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . ".html") && strlen(preg_replace("/[^a-zA-Z0-9]/m", "", file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . ".html"))) > 200,
+ file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . "-private.html") && strlen(preg_replace("/[^a-zA-Z0-9]/m", "", file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . "-private.html"))) > 200,
+ isset($member["banner"]),
+ file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/uploads/pt-" . $member["name"] . ".png"),
+ isset($member["color"])
+ ];
+
+ $result = [
+ "values" => $values,
+ "pages" => [],
+ "characters" => [],
+ "score" => (float)array_reduce($values, function ($a, $b) {
+ return $a + $b;
+ }),
+ "score2" => (float)array_reduce($values, function ($a, $b) {
+ return $a + $b;
+ }),
+ "actions" => []
+ ];
+
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . ".html") && strlen(preg_replace("/[^a-zA-Z0-9]/m", "", file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . ".html"))) <= 200) {
+ $result["pages"][] = true;
+ $result["score"] += strlen(preg_replace("/[^a-zA-Z0-9]/m", "", file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . ".html"))) / 201;
+ } else {
+ $result["pages"][] = false;
+ }
+
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . "-private.html") && strlen(preg_replace("/[^a-zA-Z0-9]/m", "", file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . "-private.html"))) <= 200) {
+ $result["pages"][] = true;
+ $result["score"] += strlen(preg_replace("/[^a-zA-Z0-9]/m", "", file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . "-private.html"))) / 201;
+ } else {
+ $result["pages"][] = false;
+ }
+
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . ".html")) {
+ $result["characters"][] = strlen(preg_replace("/[^a-zA-Z0-9]/m", "", file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . ".html")));
+ } else {
+ $result["characters"][] = -1;
+ }
+
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . "-private.html")) {
+ $result["characters"][] = strlen(preg_replace("/[^a-zA-Z0-9]/m", "", file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/content/" . $member["id"] . "-private.html")));
+ } else {
+ $result["characters"][] = -1;
+ }
+
+ if (!((isset($member["_metadata"]["birth"]["age"]) && $member["_metadata"]["birth"]["age"] > 0) || isset($member["_metadata"]["birth"]["year"]) && $member["_metadata"]["birth"]["year"] > 1900) && !(isset($member["_metadata"]["birth"]["age"]) && $member["_metadata"]["birth"]["age"] === -1) && in_array("alicorn", $member["_metadata"]["species"])) {
+ $result["score"] += 0.5;
+ }
+
+ $result["progress"] = $result["score"] / 7;
+ $result["sortable"] = (int)($result["progress"] * 100000000000000);
+
+ if (!$result["values"][5]) {
+ $result["actions"][] = "a Pony Town character";
+ }
+
+ if (!$result["values"][6]) {
+ $result["actions"][] = "a color";
+ }
+
+ if (!$result["values"][2]) {
+ if ($result["characters"][0] === -1) {
+ $result["actions"][] = "a public page";
+ } else {
+ $result["actions"][] = (200 - $result["characters"][0]) . " characters to the public page";
+ }
+ }
+
+ if (!$result["values"][3]) {
+ if ($result["characters"][1] === -1) {
+ $result["actions"][] = "a private page";
+ } else {
+ $result["actions"][] = (200 - $result["characters"][1]) . " characters to the private page";
+ }
+ }
+
+ if (!$result["values"][4]) {
+ $result["actions"][] = "a banner";
+ }
+
+ if (!$result["values"][0]) {
+ $result["actions"][] = "an age";
+ }
+
+ if (!$result["values"][1]) {
+ $result["actions"][] = "a birthdate";
+ }
+
+ return $result;
+} \ No newline at end of file
diff --git a/includes/util/pronouns.inc b/includes/util/pronouns.inc
new file mode 100644
index 0000000..6ab2487
--- /dev/null
+++ b/includes/util/pronouns.inc
@@ -0,0 +1,149 @@
+<?php
+
+$pronounsSets = [
+ "pony" => [
+ "gender" => "ponygender",
+ "gender:fr" => "poneygenre",
+ "object" => "pony",
+ "person" => "pony",
+ "possessive_det" => "pony's",
+ "possessive_pro" => "pony's",
+ "reflexive" => "ponyself",
+ "subjective" => "pony",
+ "third" => true,
+ "color" => "warning"
+ ],
+ "she" => [
+ "gender" => "female",
+ "gender:fr" => "fille",
+ "object" => "her",
+ "person" => "girl",
+ "possessive_det" => "her",
+ "possessive_pro" => "hers",
+ "reflexive" => "herself",
+ "subjective" => "she",
+ "third" => true,
+ "color" => "success"
+ ],
+ "he" => [
+ "gender" => "male",
+ "gender:fr" => "garçon",
+ "object" => "him",
+ "person" => "boy",
+ "possessive_det" => "his",
+ "possessive_pro" => "his",
+ "reflexive" => "himself",
+ "subjective" => "he",
+ "third" => true,
+ "color" => "info"
+ ],
+ "it" => [
+ "gender" => "agender",
+ "gender:fr" => "agenre",
+ "object" => "it",
+ "person" => "person",
+ "possessive_det" => "its",
+ "possessive_pro" => "its",
+ "reflexive" => "itself",
+ "subjective" => "it",
+ "third" => true,
+ "color" => "light"
+ ],
+ "they" => [
+ "gender" => "non binary",
+ "gender:fr" => "non binaire",
+ "object" => "them",
+ "person" => "person",
+ "possessive_det" => "their",
+ "possessive_pro" => "theirs",
+ "reflexive" => "themself",
+ "subjective" => "they",
+ "third" => false,
+ "color" => "primary"
+ ]
+];
+
+$pronounGetCount = 0;
+
+$possibilitiesPerSet = [];
+foreach ($pronounsSets as $name => $set) {
+ if (!isset($possibilitiesPerSet[$name])) $possibilitiesPerSet[$name] = [];
+ $possibilitiesPerSet[$name][] = $name;
+
+ foreach ($set as $category => $value) {
+ if (is_string($value)) $possibilitiesPerSet[$name][] = $value;
+ }
+}
+
+function getSetFromValue(string $value) {
+ global $possibilitiesPerSet;
+
+ foreach ($possibilitiesPerSet as $name => $set) {
+ if (in_array($value, $set)) {
+ return $name;
+ }
+ }
+
+ return null;
+}
+
+function getPronounsFromMark(?string $mark = null): array {
+ if (!isset($mark) || trim($mark) === "") {
+ return ["they"];
+ } else {
+ $parts = array_unique(array_map(function ($i) {
+ return getSetFromValue($i);
+ }, explode("/", $mark)));
+ return $parts;
+ }
+}
+
+function getMemberPronouns(?string $pronouns): ?array {
+ global $pronounsSets;
+ $list = getPronounsFromMark($pronouns);
+ return $pronounsSets[$list[array_rand($list)]] ?? $pronounsSets["she"];
+}
+
+function getGenderFromPronoun(string $pronoun, bool $french = false) {
+ global $pronounsSets;
+ $set = getPronounsFromMark($pronoun)[0];
+
+ if ($french) {
+ return ($pronounsSets[$set] ?? $pronounsSets["they"])["gender:fr"];
+ } else {
+ return ($pronounsSets[$set] ?? $pronounsSets["they"])["gender"];
+ }
+}
+
+function pronounInFrench(string $pronoun): string {
+ return match ($pronoun) {
+ "she" => "elle",
+ "her" => "elle",
+ "he" => "il",
+ "him" => "lui",
+ "they" => "iel",
+ "them" => "iel",
+ "it" => "iel",
+ "its" => "iel",
+ "pony" => "poney",
+ "pony's" => "à poney",
+ "ponys" => "à poney",
+ default => $pronoun,
+ };
+}
+
+function getTooltipsFromMark(string $mark = null, bool $french = false): ?string {
+ if (!isset($mark)) {
+ return null;
+ } else {
+ if ($french) {
+ return implode("/", array_map(function ($i) {
+ return "<span title='" . ucfirst(getGenderFromPronoun($i, true)) . "' data-bs-toggle='tooltip'>" . pronounInFrench($i) . "</span>";
+ }, explode("/", $mark)));
+ } else {
+ return implode("/", array_map(function ($i) {
+ return "<span title='" . ucfirst(getGenderFromPronoun($i, false)) . "' data-bs-toggle='tooltip'>" . $i . "</span>";
+ }, explode("/", $mark)));
+ }
+ }
+} \ No newline at end of file
diff --git a/includes/util/rainbow.inc b/includes/util/rainbow.inc
new file mode 100644
index 0000000..66ac2fc
--- /dev/null
+++ b/includes/util/rainbow.inc
@@ -0,0 +1,56 @@
+<?php
+
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/functions.inc";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/bitset.inc";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/score.inc";
+
+function rainbow($hideCloudburst = false): array {
+ $members = scoreOrderGlobal();
+
+ if ($hideCloudburst) {
+ $members = array_filter($members, function ($i) {
+ return $i["_system"] === "gdapd";
+ });
+ }
+
+ $data = [];
+
+ foreach ($members as $member) {
+ $data[$member["name"]] = [
+ "_data" => $member
+ ];
+
+ if (isset($member["color"])) {
+ $data[$member["name"]]["rgb"] = [
+ hexdec(substr($member["color"], 0, 2)),
+ hexdec(substr($member["color"], 2, 2)),
+ hexdec(substr($member["color"], 4, 2))
+ ];
+ $data[$member["name"]]["hsl"] = rgbToHsl(
+ $data[$member["name"]]["rgb"][0],
+ $data[$member["name"]]["rgb"][1],
+ $data[$member["name"]]["rgb"][2]
+ );
+ } else {
+ $data[$member["name"]]["rgb"] = [255, 255, 255];
+ $data[$member["name"]]["hsl"] = rgbToHsl(255, 255, 255);
+ }
+ }
+
+ return $data;
+}
+
+function getMembersByColor($hideCloudburst = false): array {
+ $members = rainbow($hideCloudburst);
+ uasort($members, function ($a, $b) {
+ return $a['hsl'][0] - $b['hsl'][0];
+ });
+
+ $sorted = [];
+ foreach ($members as $data) {
+ $data["_data"]["hue"] = $data["hsl"][0];
+ $sorted[] = $data["_data"];
+ }
+
+ return $sorted;
+} \ No newline at end of file
diff --git a/includes/util/random.inc b/includes/util/random.inc
new file mode 100644
index 0000000..b6e7905
--- /dev/null
+++ b/includes/util/random.inc
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * @throws Exception
+ */
+function random($length = 13): string {
+ if (function_exists("random_bytes")) {
+ $bytes = random_bytes(ceil($length / 2));
+ } elseif (function_exists("openssl_random_pseudo_bytes")) {
+ $bytes = openssl_random_pseudo_bytes(ceil($length / 2));
+ } else {
+ throw new Exception("No cryptographically secure random function available");
+ }
+ return substr(bin2hex($bytes), 0, $length);
+} \ No newline at end of file
diff --git a/includes/util/score.inc b/includes/util/score.inc
new file mode 100644
index 0000000..57c7a75
--- /dev/null
+++ b/includes/util/score.inc
@@ -0,0 +1,136 @@
+<?php
+
+function scoreOrder($members, $system, $useDominant = false) {
+ $ordered = [];
+ foreach ($members as $member) {
+ if ($member["name"] !== "unknown" && $member["name"] !== "fusion" && $member["name"] !== "new" && !str_starts_with($member["name"], "smol") && !str_ends_with($member["name"], "-travelling") && file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/metadata/$member[id].json")) {
+ if (isset($member["color"])) {
+ if ($useDominant) {
+ $rgb = [
+ hexdec(substr($member["dominant_color"] ?? $member["color"], 0, 2)),
+ hexdec(substr($member["dominant_color"] ?? $member["color"], 2, 2)),
+ hexdec(substr($member["dominant_color"] ?? $member["color"], 4, 2))
+ ];
+ } else {
+ $rgb = [
+ hexdec(substr($member["color"], 0, 2)),
+ hexdec(substr($member["color"], 2, 2)),
+ hexdec(substr($member["color"], 4, 2))
+ ];
+ }
+
+ $hsl = rgbToHsl(
+ $rgb[0],
+ $rgb[1],
+ $rgb[2]
+ );
+ } else {
+ $rgb = [255, 255, 255];
+ $hsl = rgbToHsl(255, 255, 255);
+ }
+
+ $systemID = $member["system"] ?? $system;
+
+ $member["_system"] = $member["system"] = $systemID;
+ $member["_metadata"] = parseMetadata(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/metadata/$member[id].json"), true));
+ $member["_score"] = $hsl[0];
+ $ordered[] = $member;
+ }
+ }
+
+ uasort($ordered, function($a, $b) {
+ return $a["_score"] - $b["_score"];
+ });
+
+ return $ordered;
+}
+
+function scoreOrderGlobal() {
+ global $isLowerLoggedIn;
+ global $isLoggedIn;
+
+ $ordered = [];
+ foreach (json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd/members.json"), true) as $member) {
+ if ($member["name"] !== "unknown" && $member["name"] !== "fusion" && $member["name"] !== "new" && !str_starts_with($member["name"], "smol") && !str_ends_with($member["name"], "-travelling") && file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/metadata/$member[id].json")) {
+ if (isset($member["color"])) {
+ $rgb = [
+ hexdec(substr($member["color"], 0, 2)),
+ hexdec(substr($member["color"], 2, 2)),
+ hexdec(substr($member["color"], 4, 2))
+ ];
+ $hsl = rgbToHsl(
+ $rgb[0],
+ $rgb[1],
+ $rgb[2]
+ );
+ } else {
+ $rgb = [255, 255, 255];
+ $hsl = rgbToHsl(255, 255, 255);
+ }
+ $member["_score"] = $hsl[0];
+ $member["_system"] = "gdapd";
+ $member["system"] = "gdapd";
+ $member["_metadata"] = parseMetadata(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/metadata/$member[id].json"), true));
+ $ordered[] = $member;
+ }
+ }
+
+ foreach (json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc/members.json"), true) as $member) {
+ if ($member["name"] !== "unknown" && $member["name"] !== "fusion" && $member["name"] !== "new" && !str_starts_with($member["name"], "smol") && !str_ends_with($member["name"], "-travelling") && file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/metadata/$member[id].json")) {
+ if (isset($member["color"])) {
+ $rgb = [
+ hexdec(substr($member["color"], 0, 2)),
+ hexdec(substr($member["color"], 2, 2)),
+ hexdec(substr($member["color"], 4, 2))
+ ];
+ $hsl = rgbToHsl(
+ $rgb[0],
+ $rgb[1],
+ $rgb[2]
+ );
+ } else {
+ $rgb = [255, 255, 255];
+ $hsl = rgbToHsl(255, 255, 255);
+ }
+ $member["_score"] = $hsl[0];
+ $member["_system"] = "ynmuc";
+ $member["system"] = "ynmuc";
+ $member["_metadata"] = parseMetadata(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/metadata/$member[id].json"), true));
+ $ordered[] = $member;
+ }
+ }
+
+ if ($isLowerLoggedIn || $isLoggedIn) {
+ $app = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true);
+ foreach (json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/other/members.json"), true) as $member) {
+ if ($member["name"] !== "unknown" && $member["name"] !== "fusion" && $member["name"] !== "new" && !str_starts_with($member["name"], "smol") && !str_ends_with($member["name"], "-travelling") && file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/metadata/$member[id].json")) {
+ if (isset($member["color"])) {
+ $rgb = [
+ hexdec(substr($member["color"], 0, 2)),
+ hexdec(substr($member["color"], 2, 2)),
+ hexdec(substr($member["color"], 4, 2))
+ ];
+ $hsl = rgbToHsl(
+ $rgb[0],
+ $rgb[1],
+ $rgb[2]
+ );
+ } else {
+ $rgb = [255, 255, 255];
+ $hsl = rgbToHsl(255, 255, 255);
+ }
+ $member["_score"] = $hsl[0];
+ $member["_system"] = $app["other"]["id"];
+ $member["system"] = $app["other"]["id"];
+ $member["_metadata"] = parseMetadata(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/metadata/$member[id].json"), true));
+ $ordered[] = $member;
+ }
+ }
+ }
+
+ uasort($ordered, function($a, $b) {
+ return $a["_score"] - $b["_score"];
+ });
+
+ return array_values($ordered);
+} \ No newline at end of file
diff --git a/includes/util/session.inc b/includes/util/session.inc
new file mode 100644
index 0000000..7a6b931
--- /dev/null
+++ b/includes/util/session.inc
@@ -0,0 +1,29 @@
+<?php
+
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/functions.inc";
+
+global $isLoggedIn;
+global $isLowerLoggedIn;
+global $_PROFILE;
+
+$isLoggedIn = false;
+$isLowerLoggedIn = false;
+
+if (isset($_COOKIE['PEH2_SESSION_TOKEN'])) {
+ if (!(str_contains($_COOKIE['PEH2_SESSION_TOKEN'], ".") || str_contains($_COOKIE['PEH2_SESSION_TOKEN'], "/") || trim($_COOKIE["PEH2_SESSION_TOKEN"]) === "")) {
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens/" . str_replace(".", "", str_replace("/", "", $_COOKIE['PEH2_SESSION_TOKEN'])))) {
+ $_PROFILE = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens/" . str_replace(".", "", str_replace("/", "", $_COOKIE['PEH2_SESSION_TOKEN']))), true);
+
+ if (isset($_GET['invert'])) {
+ $_PROFILE["login"] = $_PROFILE["login"] === "raindrops" ? "cloudburst" : "raindrops";
+ $_PROFILE["name"] = $_PROFILE["name"] === "Raindrops System" ? "Cloudburst System" : "Raindrops System";
+ }
+
+ $isLoggedIn = true;
+ } elseif (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/lowertokens/" . str_replace(".", "", str_replace("/", "", $_COOKIE['PEH2_SESSION_TOKEN'])))) {
+ $_PROFILE = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/lowertokens/" . str_replace(".", "", str_replace("/", "", $_COOKIE['PEH2_SESSION_TOKEN']))), true);
+
+ $isLowerLoggedIn = true;
+ }
+ }
+} \ No newline at end of file
diff --git a/includes/util/short.inc b/includes/util/short.inc
new file mode 100644
index 0000000..39e42b0
--- /dev/null
+++ b/includes/util/short.inc
@@ -0,0 +1,55 @@
+<?php
+
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/functions.inc";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/bitset.inc";
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/score.inc";
+
+global $toplevel; global $lang; global $pages;
+
+header("Content-Type: text/plain");
+$members = scoreOrderGlobal();
+$list = [
+ "rd" => "/raindrops",
+ "cb" => "/cloudburst",
+ "minty" => "/cloudydreams",
+ "twilight" => "/twi",
+ "luna" => "/princessluna",
+ "cloudy" => "/cloudydreams",
+ "zipp" => "/zippstorm",
+ "babs" => "/babsseed",
+ "frost" => "/frostcrystals",
+ "violet" => "/violetdawn"
+];
+
+foreach ($members as $member) {
+ for ($i = 1; $i < strlen($member["name"]); $i++) {
+ $part = substr($member["name"], 0, $i);
+
+ if (in_array($part, array_keys($list))) {
+ $list[$part] = false;
+ } else {
+ $list[$part] = "/" . $member["name"];
+ }
+ }
+
+ foreach ($member["proxy_tags"] as $proxy) {
+ $system = $member["_system"] === "gdapd" ? "rd" : "cb";
+ $list[$system . preg_replace("/[^a-z]/m", "", $proxy["prefix"])] = "/" . $member["name"];
+ }
+
+ $list[$member["id"]] = "/" . $member["name"];
+ $list[$member["uuid"]] = "/" . $member["name"];
+}
+
+$list["minty"] = "/cloudydreams";
+$list["twilight"] = "/twi";
+
+if (in_array($toplevel, array_keys($list)) && $list[$toplevel]) {
+ if ($toplevel !== "unknown") {
+ header("Location: " . $list[$toplevel]);
+ } else {
+ peh_error("Page not found: " . strip_tags($toplevel), 404);
+ }
+} else {
+ peh_error("Page not found: " . strip_tags($toplevel), 404);
+} \ No newline at end of file
diff --git a/includes/util/travelling.inc b/includes/util/travelling.inc
new file mode 100644
index 0000000..0d1696a
--- /dev/null
+++ b/includes/util/travelling.inc
@@ -0,0 +1,62 @@
+<?php
+
+$app = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true);
+$travelling = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/travelling/travelling.json"), true);
+
+$json_cloudburst = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc/members.json"), true);
+$json_raindrops = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd/members.json"), true);
+$json_other = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/other/members.json"), true);
+
+if (!isset($json_cloudburst)) $json_cloudburst = [];
+if (!isset($json_raindrops)) $json_raindrops = [];
+if (!isset($json_other)) $json_other = [];
+
+$members = [...array_map(function ($i) {
+ $i["_system"] = "ynmuc";
+ return $i;
+}, $json_cloudburst), ...array_map(function ($i) {
+ $i["_system"] = "gdapd";
+ return $i;
+}, $json_raindrops), ...array_map(function ($i) use ($app) {
+ $i["_system"] = $app["other"]["id"];
+ return $i;
+}, $json_other)];
+
+foreach ($members as $member) {
+ if (!isset($travelling[$member["id"]])) {
+ $travelling[$member["id"]] = [
+ "travelling" => false,
+ "history" => []
+ ];
+
+ @file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/travelling/travelling.json", utf8_encode(json_encode($travelling, JSON_PRETTY_PRINT)));
+ }
+
+ if (!isset($travelling[$member["id"]]["equestria"])) {
+ $travelling[$member["id"]]["equestria"] = false;
+ }
+}
+
+function withTravelers(array $members, string $system): array {
+ global $travelling;
+ global $app;
+
+ if ($system === $app["other"]["id"]) {
+ return $members;
+ } else {
+ return [
+ ...array_map(function ($i) use ($system) {
+ $i['system'] = $system;
+ return $i;
+ }, array_filter($members, function ($i) use ($travelling) {
+ return !(isset($travelling[$i['id']]) && $travelling[$i['id']]['travelling'] && (!isset($travelling[$i['id']]['equestria']) || !$travelling[$i['id']]['equestria']));
+ })),
+ ...array_filter(array_map(function ($i) use ($system) {
+ $i['system'] = $system === "gdapd" ? "ynmuc" : "gdapd";
+ return $i;
+ }, json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/" . ($system === "gdapd" ? "ynmuc" : "gdapd") . "/members.json"), true)), function ($i) use ($travelling) {
+ return isset($travelling[$i['id']]) && $travelling[$i['id']]['travelling'] && (!isset($travelling[$i['id']]['equestria']) || !$travelling[$i['id']]['equestria']);
+ })
+ ];
+ }
+} \ No newline at end of file