Future/Frontend

* create php classes, each mirroring a js object
 * each frontend class implements __toString and json_serialize and as such can be directly used by the template
 * also allows for sane object creation before js screams in agony

 * usage TBD
This commit is contained in:
Sarjuuk
2025-07-26 23:47:41 +02:00
parent 58412e0491
commit bffdb9672e
17 changed files with 1075 additions and 159 deletions

View File

@@ -28,7 +28,7 @@ class AjaxGetdescription extends AjaxHandler
if (!User::canWriteGuide())
return '';
$desc = (new Markup($this->_post['description']))->stripTags();
$desc = Markup::stripTags($this->_post['description']);
return Lang::trimTextClean($desc, 120);
}

View File

@@ -198,7 +198,7 @@ class CommunityContent
foreach ($results as $r)
{
(new Markup($r['body']))->parseGlobalsFromText(self::$jsGlobals);
Markup::parseTags($r['body'], self::$jsGlobals);
$reply = array(
'commentid' => $commentId,
@@ -359,7 +359,7 @@ class CommunityContent
$i = 0;
foreach ($results as $r)
{
(new Markup($r['body']))->parseGlobalsFromText(self::$jsGlobals);
Markup::parseTags($r['body'], self::$jsGlobals);
self::$jsGlobals[Type::USER][$r['userId']] = $r['userId'];
@@ -384,7 +384,7 @@ class CommunityContent
$c['responseroles'] = $r['responseRoles'];
$c['responseuser'] = $r['responseUser'];
(new Markup($r['responseBody']))->parseGlobalsFromText(self::$jsGlobals);
Markup::parseTags($r['responseBody'], self::$jsGlobals);
}
if ($r['editCount']) // lastEdit

View File

@@ -0,0 +1,69 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Announcement implements \JsonSerializable
{
public const MODE_PAGE_TOP = 0;
public const MODE_CONTENT_TOP = 1;
public const STATUS_DISABLED = 0;
public const STATUS_ENABLED = 1;
public const STATUS_DELETED = 2;
public readonly int $status;
private bool $editable = false;
public function __construct(
public readonly int $id,
private string $name,
private LocString $text,
private int $mode = self::MODE_CONTENT_TOP,
int $status = self::STATUS_ENABLED,
private string $style = '')
{
// a negative id displays ENABLE/DISABLE and DELETE links for this announcement
// TODO - the ugroup check mirrors the js. Add other checks like ownership status? (ownership currently not stored)
if (User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU) /* && User::$id == $authorId */)
$this->editable = true;
if ($this->mode != self::MODE_PAGE_TOP && $this->mode != self::MODE_CONTENT_TOP)
$this->mode = self::MODE_PAGE_TOP;
if ($status != self::STATUS_DISABLED && $status != self::STATUS_ENABLED && $status != self::STATUS_DELETED)
$this->status = self::STATUS_DELETED;
else
$this->status = $status;
}
public function jsonSerialize() : array
{
$json = array(
'parent' => 'announcement-' . abs($this->id),
'id' => $this->editable ? -$this->id : $this->id,
'mode' => $this->mode,
'status' => $this->status,
'name' => $this->name,
'text' => (string)$this->text // force LocString to naive string for display
);
if ($this->style)
$json['style'] = $this->style;
return $json;
}
public function __toString() : string
{
if ($this->status == self::STATUS_DELETED)
return '';
return "new Announcement(".Util::toJSON($this).");\n";
}
}
?>

View File

@@ -0,0 +1,50 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Book implements \JsonSerializable
{
public function __construct(
private array $pages, // js:array of html
private string $parent = 'book-generic', // HTMLNode.id
private ?int $page = null) // start page; defaults to 1
{
if (!$this->parent)
trigger_error(self::class.'::__construct - initialized without parent element', E_USER_WARNING);
if (!$this->pages)
trigger_error(self::class.'::__construct - initialized without content', E_USER_WARNING);
else
$this->pages = Util::parseHtmlText($this->pages);
}
public function &iterate() : \Generator
{
reset($this->pages);
foreach ($this->pages as $idx => &$page)
yield $idx => $page;
}
public function jsonSerialize() : array
{
$result = [];
foreach ($this as $prop => $val)
if ($val !== null && $prop[0] != '_')
$result[$prop] = $val;
return $result;
}
public function __toString() : string
{
return "new Book(".Util::toJSON($this).");\n";
}
}
?>

View File

@@ -0,0 +1,159 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class IconElement
{
public const SIZE_SMALL = 0;
public const SIZE_MEDIUM = 1;
public const SIZE_LARGE = 2;
private const CREATE_ICON_TPL = "\$WH.ge('%s%d').appendChild(%s.createIcon(%s));\n";
private int $idx = 0;
private string $href = '';
private bool $noIcon = false;
public readonly string $quality;
public readonly ?string $align;
public readonly int $size;
public function __construct(
public readonly int $type,
public readonly int $typeId,
public readonly string $text,
public readonly int|string $num = '',
public readonly int|string $qty = '',
?string $quality = null,
int $size = self::SIZE_MEDIUM,
bool $link = true,
string $url = '',
?string $align = null,
public readonly string $element = 'icontab-icon',
public ?string $extraText = null
)
{
if (is_numeric($quality))
$this->quality = 'q'.$quality;
else if ($quality !== null)
$this->quality = 'q';
else
$this->quality = '';
if ($size < self::SIZE_SMALL || $size > self::SIZE_LARGE)
{
trigger_error('IconElement::__construct - invalid icon size '.$size.'. Normalied to 1 [small]', E_USER_WARNING);
$this->size = self::SIZE_SMALL;
}
else
$this->size = $size;
if ($align && !in_array($align, ['left', 'right', 'center', 'justify']))
{
trigger_error('IconElement::__construct - unset invalid align value "'.$align.'".', E_USER_WARNING);
$this->align = null;
}
else
$this->align = $align;
if ($type && $typeId && !Type::validateIds($type, $typeId))
{
$link = false;
trigger_error('IconElement::__construct - invalid typeId '.$typeId.' for '.Type::getFileString($type).'.', E_USER_WARNING);
}
else if (!$type || !$typeId)
$link = false;
if ($link || $url)
$this->href = $url ?: '?'.Type::getFileString($this->type).'='.$this->typeId;
// see Spell/Tools having icon container but no actual icon and having to be inline with other IconElements
$this->noIcon = !$typeId || !Type::hasIcon($type);
}
public function renderContainer(int $lpad = 0, int &$iconIdxOffset = 0, bool $rowWrap = false) : string
{
if (!$this->noIcon)
$this->idx = ++$iconIdxOffset;
$dom = new \DOMDocument('1.0', 'UTF-8');
$td = $dom->createElement('td');
$th = $dom->createElement('th');
if ($this->noIcon) // see Spell/Tools or AchievementCriteria having no actual icon, but placeholder
{
$ul = $dom->createElement('ul');
$li = $dom->createElement('li');
$var = $dom->createElement('var', ' ');
$li->appendChild($var);
$ul->appendChild($li);
$th->appendChild($ul);
}
else
{
$th->setAttribute('id', $this->element . $this->idx);
if ($this->align)
$th->setAttribute('align', $this->align);
}
if ($this->href)
($a = $dom->createElement('a', $this->text))->setAttribute('href', $this->href);
else
$a = $dom->createTextNode($this->text);
if ($this->quality)
{
($sp = $dom->createElement('span'))->setAttribute('class', $this->quality);
$sp->appendChild($a);
$td->appendChild($sp);
}
else
$td->appendChild($a);
// extraText can be HTML, so import it as a fragment
if ($this->extraText)
{
$fragment = $dom->createDocumentFragment();
$fragment->appendXML(' '.$this->extraText);
$td->appendChild($fragment);
}
// only for objectives list..?
if ($this->num && $this->size == self::SIZE_SMALL)
$td->appendChild($dom->createTextNode(' ('.$this->num.')'));
if ($rowWrap)
{
$tr = $dom->createElement('tr');
$tr->appendChild($th);
$tr->appendChild($td);
$dom->append($tr);
}
else
$dom->append($th, $td);
return str_repeat(' ', $lpad) . $dom->saveHTML();
}
// $WH.ge('icontab-icon1').appendChild(g_spells.createIcon(40120, 1, '1-4', 0));
public function renderJS(int $lpad = 0) : string
{
if ($this->noIcon)
return '';
$params = [$this->typeId, $this->size];
if ($this->num || $this->qty)
$params[] = is_numeric($this->num) ? $this->num : "'".$this->num."'";
if ($this->qty)
$params[] = is_numeric($this->qty) ? $this->qty : "'".$this->qty."'";
return str_repeat(' ', $lpad) . sprintf(self::CREATE_ICON_TPL, $this->element, $this->idx, Type::getJSGlobalString($this->type), implode(', ', $params));
}
}
?>

View File

@@ -0,0 +1,49 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class InfoboxMarkup extends Markup
{
public function __construct(private array $items = [], array $opts, string $parent = '')
{
parent::__construct('', $opts, $parent);
}
public function addItem(string $item, ?int $pos = null) : void
{
if (is_null($pos) || $pos >= count($this->items))
$this->items[] = $item;
else
array_splice($this->items, $pos, 0, $item);
}
public function append(string $text) : self
{
if ($this->items && !$this->__text)
$this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]');
return parent::append($text);
}
public function __toString() : string
{
if ($this->items && !$this->__text)
$this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]');
return parent::__toString();
}
public function getJsGlobals() : array
{
if ($this->items && !$this->__text)
$this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]');
return parent::getJsGlobals();
}
}
?>

View File

@@ -0,0 +1,174 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Listview implements \JsonSerializable
{
public const MODE_DEFAULT = 0;
public const MODE_CHECKBOX = 1;
public const MODE_DIV = 2;
public const MODE_TILED = 3;
public const MODE_CALENDAR = 4;
public const MODE_FLEXGRID = 5;
private const TEMPLATES = array(
'achievement' => ['template' => 'achievement', 'id' => 'achievements', 'name' => '$LANG.tab_achievements' ],
'areatrigger' => ['template' => 'areatrigger', 'id' => 'areatrigger', ],
'calendar' => ['template' => 'holidaycal', 'id' => 'calendar', 'name' => '$LANG.tab_calendar' ],
'class' => ['template' => 'classs', 'id' => 'classes', 'name' => '$LANG.tab_classes' ],
'commentpreview' => ['template' => 'commentpreview', 'id' => 'comments', 'name' => '$LANG.tab_comments' ],
'npc' => ['template' => 'npc', 'id' => 'npcs', 'name' => '$LANG.tab_npcs' ],
'currency' => ['template' => 'currency', 'id' => 'currencies', 'name' => '$LANG.tab_currencies' ],
'emote' => ['template' => 'emote', 'id' => 'emotes', ],
'enchantment' => ['template' => 'enchantment', 'id' => 'enchantments', ],
'event' => ['template' => 'holiday', 'id' => 'holidays', 'name' => '$LANG.tab_holidays' ],
'faction' => ['template' => 'faction', 'id' => 'factions', 'name' => '$LANG.tab_factions' ],
'genericmodel' => ['template' => 'genericmodel', 'id' => 'same-model-as', 'name' => '$LANG.tab_samemodelas' ],
'icongallery' => ['template' => 'icongallery', 'id' => 'icons', ],
'item' => ['template' => 'item', 'id' => 'items', 'name' => '$LANG.tab_items' ],
'itemset' => ['template' => 'itemset', 'id' => 'itemsets', 'name' => '$LANG.tab_itemsets' ],
'mail' => ['template' => 'mail', 'id' => 'mails', ],
'model' => ['template' => 'model', 'id' => 'gallery', 'name' => '$LANG.tab_gallery' ],
'object' => ['template' => 'object', 'id' => 'objects', 'name' => '$LANG.tab_objects' ],
'pet' => ['template' => 'pet', 'id' => 'hunter-pets', 'name' => '$LANG.tab_pets' ],
'profile' => ['template' => 'profile', 'id' => 'profiles', 'name' => '$LANG.tab_profiles' ],
'quest' => ['template' => 'quest', 'id' => 'quests', 'name' => '$LANG.tab_quests' ],
'race' => ['template' => 'race', 'id' => 'races', 'name' => '$LANG.tab_races' ],
'replypreview' => ['template' => 'replypreview', 'id' => 'comment-replies', 'name' => '$LANG.tab_commentreplies'],
'reputationhistory' => ['template' => 'reputationhistory', 'id' => 'reputation', 'name' => '$LANG.tab_reputation' ],
'screenshot' => ['template' => 'screenshot', 'id' => 'screenshots', 'name' => '$LANG.tab_screenshots' ],
'skill' => ['template' => 'skill', 'id' => 'skills', 'name' => '$LANG.tab_skills' ],
'sound' => ['template' => 'sound', 'id' => 'sounds', 'name' => '$LANG.types[19][2]' ],
'spell' => ['template' => 'spell', 'id' => 'spells', 'name' => '$LANG.tab_spells' ],
'title' => ['template' => 'title', 'id' => 'titles', 'name' => '$LANG.tab_titles' ],
'topusers' => ['template' => 'topusers', 'id' => 'topusers', 'name' => '$LANG.topusers' ],
'video' => ['template' => 'video', 'id' => 'videos', 'name' => '$LANG.tab_videos' ],
'zone' => ['template' => 'zone', 'id' => 'zones', 'name' => '$LANG.tab_zones' ],
'guide' => ['template' => 'guide', 'id' => 'guides', ]
);
private string $id = '';
private ?string $name = null;
private ?array $data = null; // js:array of object <RowDefinitions>
private ?string $tabs = null; // js:Object; instance of "Tabs"
private ?string $parent = 'lv-generic'; // HTMLNode.id; can be null but is pretty much always 'lv-generic'
private ?string $template = null;
private ?int $mode = null; // js:int; defaults to MODE_DEFAULT
private ?string $note = null; // text in top band
private ?int $poundable = null; // 0 (no); 1 (always); 2 (yes, w/o sorting); defaults to 1
private ?int $searchable = null; // js:bool; defaults to FALSE
private ?int $filtrable = null; // js:bool; defaults to FALSE
private ?int $sortable = null; // js:bool; defaults to FALSE
private ?int $searchDelay = null; // in ms; defalts to 333
private ?int $clickable = null; // js:bool; defaults to TRUE
private ?int $hideBands = null; // js:int; 1:top, 2:bottom, 3:both;
private ?int $hideNav = null; // js:int; 1:top, 2:bottom, 3:both;
private ?int $hideHeader = null; // js:bool
private ?int $hideCount = null; // js:bool
private ?int $debug = null; // js:bool
private ?int $_truncated = null; // js:bool; adds predefined note to top band, because there was too much data to display
private ?int $_errors = null; // js:bool; adds predefined note to top band, because there was an error
private ?int $_petTalents = null; // js:bool; applies modifier for talent levels
private ?int $nItemsPerPage = null; // js:int; defaults to 50
private ?int $_totalCount = null; // js:int; used by loot and comments
private ?array $clip = null; // js:array of int {w:<width>, h:<height>}
private ?string $customPound = null;
private ?string $genericlinktype = null; // sometimes set when expecting to display model
private ?array $_upgradeIds = null; // js:array of int (itemIds)
private null|array|string $extraCols = null; // js:callable or js:array of object <ColumnDefinition>
private null|array|string $visibleCols = null; // js:callable or js:array of string <colIds>
private null|array|string $hiddenCols = null; // js:callable or js:array of string <colIds>
private null|array|string $sort = null; // js:callable or js:array of colIndizes
private ?string $onBeforeCreate = null; // js:callable
private ?string $onAfterCreate = null; // js:callable
private ?string $onNoData = null; // js:callable
private ?string $computeDataFunc = null; // js:callable
private ?string $onSearchSubmit = null; // js:callable
private ?string $createNote = null; // js:callable
private ?string $createCbControls = null; // js:callable
private ?string $customFilter = null; // js:callable
private ?string $getItemLink = null; // js:callable
private ?array $sortOptions = null; // js:array of object {id:<colId>, name:<name>, hidden:<bool>, type:"text", sortFunc:<callable>}
private string $__addIn = '';
public function __construct(array $opts, string $template = '', string $addIn = '')
{
if ($template && isset(self::TEMPLATES[$template]))
foreach (self::TEMPLATES[$template] as $k => $v)
$this->$k = $v;
foreach ($opts as $k => $v)
{
if (property_exists($this, $k))
{
// reindex arrays to force json_encode to treat them as arrays
if (is_array($v)) // in_array($k, ['data', 'extraCols', 'visibleCols', 'hiddenCols', 'sort', 'sortOptions']))
$v = array_values($v);
$this->$k = $v;
}
else
trigger_error(self::class.'::__construct - unrecognized option: ' . $k);
}
if ($addIn && !Template\PageTemplate::test('listviews/', $addIn.'.tpl'))
trigger_error('Nonexistent Listview addin requested: template/listviews/'.$addIn.'.tpl', E_USER_ERROR);
else if ($addIn)
$this->__addIn = 'template/listviews/'.$addIn.'.tpl';
}
public function &iterate() : \Generator
{
reset($this->data);
foreach ($this->data as $idx => &$row)
yield $idx => $row;
}
public function getTemplate() : string
{
return $this->template;
}
public function setTabs(string $tabVar) : void
{
if ($tabVar[0] !== '$') // expects a jsVar, which we denote with a prefixed $
$tabVar = '$' . $tabVar;
$this->tabs = $tabVar;
}
public function setError() : void
{
$this->_errors = 1;
}
public function jsonSerialize() : array
{
$result = [];
foreach ($this as $prop => $val)
if ($val !== null && substr($prop, 0, 2) != '__')
$result[$prop] = $val;
return $result;
}
public function __toString() : string
{
if ($this->__addIn)
include($this->__addIn);
return "new Listview(".Util::toJSON($this).");\n";
}
}
?>

View File

@@ -0,0 +1,291 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Markup implements \JsonSerializable
{
private const DB_TAG_PATTERN = '/(?<!\\\\)\[(npc|object|item|itemset|quest|spell|zone|faction|pet|achievement|statistic|title|event|class|race|skill|currency|emote|enchantment|money|sound|icondb)=(-?\d+)[^\]]*\]/i';
// const val
public const MARKUP_MODE_COMMENT = 1;
public const MARKUP_MODE_ARTICLE = 2;
public const MARKUP_MODE_QUICKFACTS = 3;
public const MARKUP_MODE_SIGNATURE = 4;
public const MARKUP_MODE_REPLY = 5;
// js var
public const MODE_COMMENT = '$Markup.MODE_COMMENT';
public const MODE_ARTICLE = '$Markup.MODE_ARTICLE';
public const MODE_QUICKFACTS = '$Markup.MODE_QUICKFACTS';
public const MODE_SIGNATURE = '$Markup.MODE_SIGNATURE';
public const MODE_REPLY = '$Markup.MODE_REPLY';
// const val
public const MARKUP_CLASS_ADMIN = 40;
public const MARKUP_CLASS_STAFF = 30;
public const MARKUP_CLASS_PREMIUM = 20;
public const MARKUP_CLASS_USER = 10;
public const MARKUP_CLASS_PENDING = 1;
// js var
public const CLASS_ADMIN = '$Markup.CLASS_ADMIN';
public const CLASS_STAFF = '$Markup.CLASS_STAFF';
public const CLASS_PREMIUM = '$Markup.CLASS_PREMIUM';
public const CLASS_USER = '$Markup.CLASS_USER';
public const CLASS_PENDING = '$Markup.CLASS_PENDING';
// options
private ?string $prepend = null; // html in front of article
private ?string $append = null; // html trailing the article
private ?int $locale = null; // forces tooltips in the article to adhere to another locale
private ?int $inBlog = null; // js:bool; unused by aowow
private ?string $mode = null; // defaults to Markup.MODE_ARTICLE, which is what we want.
private ?string $allow = null; // defaults to Markup.CLASS_STAFF
private ?int $roles = null; // if allow is null, get allow from roles (user group); also mode will be set to MODE_ARTICLE for staff groups
private ?int $stopAtBreak = null; // js:bool; only parses text until substring "[break]" is encountered; some debug option...?
private ?string $highlight = null; // HTMLNode selector
private ?int $skipReset = null; // js:bool; unsure, if TRUE the next settings in this block get skipped
private ?string $uid = null; // defaults to 'abc'; unsure, key under which media is stored and referenced in g_screenshots and g_videos
private ?string $root = null; // unsure, something to with Markup Tags that need to be subordinate to other tags (e.g.: [li] to [ol])
private ?int $preview = null; // unsure, appends '-preview' to the div created by the [tabs] tag and prevents scrolling. Forum feature?
private ?int $dbpage = null; // js:bool; set on db type detail pages; adds article edit links to admin menu
protected string $__text;
private string $__parent = 'article-generic';
public function __construct(string $text, array $opts, string $parent = '')
{
foreach ($opts as $k => $v)
{
if (property_exists($this, $k))
$this->$k = $v;
else
trigger_error(self::class.'::__construct - unrecognized option: ' . $k);
}
$this->__text = $text;
if ($parent)
$this->__parent = $parent;
}
public function getJsGlobals() : array
{
return $this->_parseTags();
}
public function getParent() : string
{
return $this->__parent;
}
/***********************/
/* Markup tag handling */
/***********************/
private function _parseTags(array &$jsg = []) : array
{
return self::parseTags($this->__text, $jsg);
}
public static function parseTags(string $text, array &$jsg = []) : array
{
$jsGlobals = [];
if (preg_match_all(self::DB_TAG_PATTERN, $text, $matches, PREG_SET_ORDER))
{
foreach ($matches as $match)
{
if ($match[1] == 'statistic')
$match[1] = 'achievement';
else if ($match[1] == 'icondb')
$match[1] = 'icon';
if ($match[1] == 'money')
{
if (stripos($match[0], 'items'))
{
if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i+=2)
$jsGlobals[Type::ITEM][$sm[$i]] = $sm[$i];
}
}
if (stripos($match[0], 'currency'))
{
if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i+=2)
$jsGlobals[Type::CURRENCY][$sm[$i]] = $sm[$i];
}
}
}
else if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1]))
$jsGlobals[$type][$match[2]] = $match[2];
}
}
Util::mergeJsGlobals($jsg, $jsGlobals);
return $jsGlobals;
}
private function _stripTags(array $jsgData = []) : string
{
return self::stripTags($this->__text, $jsgData);
}
public static function stripTags(string $text, array $jsgData = []) : string
{
// replace DB Tags
$text = preg_replace_callback(self::DB_TAG_PATTERN, function ($match) use ($jsgData) {
if ($match[1] == 'statistic')
$match[1] = 'achievement';
else if ($match[1] == 'icondb')
$match[1] = 'icon';
else if ($match[1] == 'money')
{
$moneys = [];
if (stripos($match[0], 'items'))
{
if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i += 2)
{
if (!empty($jsgData[Type::ITEM][1][$sm[$i]]))
$moneys[] = $jsgData[Type::ITEM][1][$sm[$i]]['name'];
else
$moneys[] = Util::ucFirst(Lang::game('item')).' #'.$sm[$i];
}
}
}
if (stripos($match[0], 'currency'))
{
if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i += 2)
{
if (!empty($jsgData[Type::CURRENCY][1][$sm[$i]]))
$moneys[] = $jsgData[Type::CURRENCY][1][$sm[$i]]['name'];
else
$moneys[] = Util::ucFirst(Lang::game('curency')).' #'.$sm[$i];
}
}
}
return Lang::concat($moneys);
}
if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1]))
{
if (!empty($jsgData[$type][1][$match[2]]))
return $jsgData[$type][1][$match[2]]['name'];
else
return Util::ucFirst(Lang::game($match[1])).' #'.$match[2];
}
trigger_error('Markup::stripTags() - encountered unhandled db-tag: '.var_export($match));
return '';
}, $text);
// replace line endings
$text = str_replace('[br]', "\n", $text);
// strip other Tags
$stripped = '';
$inTag = false;
for ($i = 0; $i < strlen($text); $i++)
{
if ($text[$i] == '[' && (!$i || $text[$i - 1] != '\\'))
$inTag = true;
if (!$inTag)
$stripped .= $text[$i];
if ($inTag && $text[$i] == ']' && (!$i || $text[$i - 1] != '\\'))
$inTag = false;
}
return $stripped;
}
/*********************/
/* String Operations */
/*********************/
public function append(string $text) : self
{
$this->__text .= $text;
return $this;
}
public function prepend(string $text) : self
{
$this->__text = $text . $this->__text;
return $this;
}
public function apply(\Closure $fn) : void
{
$this->__text = $fn($this->__text);
}
public function replace(string $middle, int $offset = 0, ?int $len = null) : self
{
// y no mb_substr_replace >:(
$start = $end = '';
if ($offset < 0)
$offset = mb_strlen($this->__text) + $offset;
$start = mb_substr($this->__text, 0, $offset);
if (!is_null($len) && $len >= 0)
$end = mb_substr($this->__text, $offset + $len);
else if (!is_null($len) && $len < 0)
$end = mb_substr($this->__text, $offset + mb_strlen($this->__text) + $len);
$this->__text = $start . $middle . $end;
return $this;
}
private function cleanText() : string
{
// break script-tags, unify newlines
$val = preg_replace(['/script\s*\>/i', "/\r\n/", "/\r/"], ['script>', "\n", "\n"], $this->__text);
return strtr(Util::jsEscape($val), ['script>' => 'scr"+"ipt>']);
}
public function jsonSerialize() : array
{
$result = [];
foreach ($this as $prop => $val)
if ($val !== null && $prop[0] != '_')
$result[$prop] = $val;
return $result;
}
public function __toString() : string
{
if ($this->jsonSerialize())
return 'Markup.printHtml("'.$this->cleanText().'", "'.$this->__parent.'", '.Util::toJSON($this).");\n";
return 'Markup.printHtml("'.$this->cleanText().'", "'.$this->__parent."\");\n";
}
}
?>

View File

@@ -0,0 +1,70 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Summary implements \JsonSerializable
{
private string $id = ''; // HTMLNode.id
private ?string $parent = ''; // HTMLNode.id; if set $id is created and attached here instead of searched for
private string $template = ''; //
private ?int $editable = null; // js:bool; defaults to TRUE
private ?int $draggable = null; // js:bool; defaults to $editable
private ?int $searchable = null; // js:bool; defaults to $editable && $draggable
private ?int $weightable = null; // js:bool; defaults to $editable
private ?int $textable = null; // js:bool; defaults to FALSE
private ?int $enhanceable = null; // js:bool; defaults to $editable
private ?int $level = null; // js:int; defaults to 80
private array $groups = []; // js:array; defaults to GET-params
private ?array $weights = null; // js:array; defaults to GET-params
public function __construct(array $opts)
{
foreach ($opts as $k => $v)
{
if (property_exists($this, $k))
$this->$k = $v;
else
trigger_error(self::class.'::__construct - unrecognized option: ' . $k);
}
if (!$this->template)
trigger_error(self::class.'::__construct - initialized without template', E_USER_WARNING);
if (!$this->id)
trigger_error(self::class.'::__construct - initialized without HTMLNode#id to reference', E_USER_WARNING);
}
public function &iterate() : \Generator
{
reset($this->groups);
foreach ($this->groups as $idx => &$group)
yield $idx => $group;
}
public function addGroup(array $group) : void
{
$this->groups[] = $group;
}
public function jsonSerialize() : array
{
$result = [];
foreach ($this as $prop => $val)
if ($val !== null && $prop[0] != '_')
$result[$prop] = $val;
return $result;
}
public function __toString() : string
{
return "new Summary(".Util::toJSON($this).");\n";
}
}
?>

View File

@@ -0,0 +1,142 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Tabs implements \JsonSerializable, \Countable
{
private array $__tabs = [];
private string $parent = ''; // HTMLNode
private ?int $poundable = null; // js:bool
private ?int $forceScroll = null; // js:bool
private ?int $noScroll = null; // js:bool
private ?string $trackable = null; // String to track in Google Analytics .. often a DB Type
private ?string $onLoad = null; // js::callable
private ?string $onShow = null; // js::callable
private ?string $onHide = null; // js::callable
public function __construct(array $opts, public readonly string $__tabVar = 'myTabs', private bool $__forceTabs = false)
{
foreach ($opts as $k => $v)
{
if (property_exists($this, $k))
$this->$k = $v;
else
trigger_error(self::class.'::__construct - unrecognized option: ' . $k);
}
}
public function &iterate() : \Generator
{
reset($this->__tabs);
foreach ($this->__tabs as $idx => &$tab)
yield $idx => $tab;
}
public function addListviewTab(Listview $lv) : void
{
$this->__tabs[] = $lv;
}
public function addDataTab(string $id, string $name, string $data) : void
{
$this->__tabs[] = ['id' => $id, 'name' => $name, 'data' => $data];
$this->__forceTabs = true; // otherwise a single DataTab could not be accessed
}
public function getDataContainer() : \Generator
{
foreach ($this->__tabs as $tab)
if (is_array($tab))
yield '<div class="text tabbed-contents" id="tab-'.$tab['id'].'" style="display:none;">'.$tab['data'].'</div>';
}
public function getFlush() : string
{
if ($this->isTabbed())
return $this->__tabVar.".flush();";
return '';
}
public function isTabbed() : bool
{
return count($this->__tabs) > 1 || $this->__forceTabs;
}
/***********************/
/* enable deep cloning */
/***********************/
public function __clone()
{
foreach ($this->__tabs as $idx => $tab)
{
if (is_array($tab))
continue;
$this->__tabs[$idx] = clone $tab;
}
}
/******************/
/* make countable */
/******************/
public function count() : int
{
return count($this->__tabs);
}
/************************/
/* make Tabs stringable */
/************************/
public function jsonSerialize() : array
{
$result = [];
foreach ($this as $prop => $val)
if ($val !== null && $prop[0] != '_')
$result[$prop] = $val;
return $result;
}
public function __toString() : string
{
$result = '';
if ($this->isTabbed())
$result .= "var ".$this->__tabVar." = new Tabs(".Util::toJSON($this).");\n";
foreach ($this->__tabs as $tab)
{
if (is_array($tab))
{
$n = $tab['name'][0] == '$' ? substr($tab['name'], 1) : "'".$tab['name']."'";
$result .= $this->__tabVar.".add(".$n.", { id: '".$tab['id']."' });\n";
}
else
{
if ($this->isTabbed())
$tab->setTabs($this->__tabVar);
$result .= $tab; // Listview::__toString here
}
}
return $result . "\n";
}
}
?>

View File

@@ -0,0 +1,60 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Tooltip implements \JsonSerializable
{
private ?string $name = null;
private ?string $tooltip = null;
private ?\StdClass $map = null; // secondary tooltip
private ?string $icon = null;
private ?int $quality = null; // icon border color coded
private ?bool $daily = null;
private ?array $spells = null;
private ?string $buff = null;
private ?array $buffspells = null;
public function __construct(private string $__powerTpl, private string $__subject, array $opts = [])
{
foreach ($opts as $k => $v)
{
if (property_exists($this, $k))
$this->$k = $v;
else
trigger_error(self::class.'::__construct - unrecognized option: ' . $k);
}
}
public function jsonSerialize() : array
{
$out = [];
$locString = Lang::getLocale()->json();
foreach ($this as $k => $v)
{
if ($v === null || $k[0] == '_')
continue;
if ($k == 'icon')
$out[$k] = rawurldecode($v);
else if ($k == 'quality' || $k == 'map' || $k == 'daily')
$out[$k] = $v;
else
$out[$k . '_' . $locString] = $v;
}
return $out;
}
public function __toString() : string
{
return sprintf($this->__powerTpl, Util::toJSON($this->__subject, JSON_AOWOW_POWER), Lang::getLocale()->value, Util::toJSON($this, JSON_AOWOW_POWER))."\n";
}
}
?>

View File

@@ -1,150 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
this is just a skeleton for now
at some point i'll need to (at least rudamentary) parse
back and forth between markup and html
*/
class Markup
{
private $text = '';
private $jsGlobals = [];
private static $dbTagPattern = '/(?<!\\\\)\[(npc|object|item|itemset|quest|spell|zone|faction|pet|achievement|statistic|title|event|class|race|skill|currency|emote|enchantment|money|sound|icondb)=(-?\d+)[^\]]*\]/i';
public function __construct($text)
{
$this->text = $text;
}
public function parseGlobalsFromText(&$jsg = [])
{
if (preg_match_all(self::$dbTagPattern, $this->text, $matches, PREG_SET_ORDER))
{
foreach ($matches as $match)
{
if ($match[1] == 'statistic')
$match[1] = 'achievement';
else if ($match[1] == 'icondb')
$match[1] = 'icon';
if ($match[1] == 'money')
{
if (stripos($match[0], 'items'))
{
if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i+=2)
$this->jsGlobals[Type::ITEM][$sm[$i]] = $sm[$i];
}
}
if (stripos($match[0], 'currency'))
{
if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i+=2)
$this->jsGlobals[Type::CURRENCY][$sm[$i]] = $sm[$i];
}
}
}
else if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1]))
$this->jsGlobals[$type][$match[2]] = $match[2];
}
}
Util::mergeJsGlobals($jsg, $this->jsGlobals);
return $this->jsGlobals;
}
public function stripTags($globals = [])
{
// since this is an article the db-tags should already be parsed
$text = preg_replace_callback(self::$dbTagPattern, function ($match) use ($globals) {
if ($match[1] == 'statistic')
$match[1] = 'achievement';
else if ($match[1] == 'icondb')
$match[1] = 'icon';
else if ($match[1] == 'money')
{
$moneys = [];
if (stripos($match[0], 'items'))
{
if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i += 2)
{
if (!empty($globals[Type::ITEM][1][$sm[$i]]))
$moneys[] = $globals[Type::ITEM][1][$sm[$i]]['name'];
else
$moneys[] = Util::ucFirst(Lang::game('item')).' #'.$sm[$i];
}
}
}
if (stripos($match[0], 'currency'))
{
if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i += 2)
{
if (!empty($globals[Type::CURRENCY][1][$sm[$i]]))
$moneys[] = $globals[Type::CURRENCY][1][$sm[$i]]['name'];
else
$moneys[] = Util::ucFirst(Lang::game('curency')).' #'.$sm[$i];
}
}
}
return Lang::concat($moneys);
}
if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1]))
{
if (!empty($globals[$type][1][$match[2]]))
return $globals[$type][1][$match[2]]['name'];
else
return Util::ucFirst(Lang::game($match[1])).' #'.$match[2];
}
trigger_error('Markup::stripTags() - encountered unhandled db-tag: '.var_export($match));
return '';
}, $this->text);
$text = str_replace('[br]', "\n", $text);
$stripped = '';
$inTag = false;
for ($i = 0; $i < strlen($text); $i++)
{
if ($text[$i] == '[')
$inTag = true;
if (!$inTag)
$stripped .= $text[$i];
if ($text[$i] == ']')
$inTag = false;
}
return $stripped;
}
public function fromHtml()
{
}
public function toHtml()
{
}
}
?>

View File

@@ -65,6 +65,8 @@ spl_autoload_register(function ($class)
if (file_exists('includes/components/'.strtolower($class).'.class.php'))
require_once 'includes/components/'.strtolower($class).'.class.php';
else if (file_exists('includes/components/frontend/'.strtolower($class).'.class.php'))
require_once 'includes/components/frontend/'.strtolower($class).'.class.php';
});
// TC systems in components

View File

@@ -72,7 +72,7 @@ class GuideList extends BaseType
$this->article[$a['rev']] = $a['article'];
if ($this->article[$a['rev']])
{
(new Markup($this->article[$a['rev']]))->parseGlobalsFromText($this->jsGlobals);
Markup::parseTags($this->article[$a['rev']], $this->jsGlobals);
return $this->article[$a['rev']];
}
else

View File

@@ -559,9 +559,9 @@ class GenericPage
if ($article)
{
if ($article['article'])
(new Markup($article['article']))->parseGlobalsFromText($this->jsgBuffer);
Markup::parseTags($article['article'], $this->jsgBuffer);
if ($article['quickInfo'])
(new Markup($article['quickInfo']))->parseGlobalsFromText($this->jsgBuffer);
Markup::parseTags($article['quickInfo'], $this->jsgBuffer);
$this->article = array(
'text' => Util::jsEscape(Util::defStatic($article['article'])),

View File

@@ -195,7 +195,7 @@ class GuidePage extends GenericPage
'specId' => $this->_post['specId'],
'title' => $this->_post['title'],
'name' => $this->_post['name'],
'description' => $this->_post['description'] ?: Lang::trimTextClean((new Markup($this->_post['body']))->stripTags(), 120),
'description' => $this->_post['description'] ?: Lang::trimTextClean(Markup::stripTags($this->_post['body']), 120),
'locale' => $this->_post['locale'],
'roles' => User::$groups,
'status' => GUIDE_STATUS_DRAFT

View File

@@ -39,7 +39,7 @@ class HomePage extends GenericPage
$this->featuredBox['text'] = Util::localizedString($this->featuredBox, 'text', true);
if ($_ = (new Markup($this->featuredBox['text']))->parseGlobalsFromText())
if ($_ = Markup::parseTags($this->featuredBox['text']))
$this->extendGlobalData($_);
if (empty($this->featuredBox['boxBG']))