Refactor project structure
This commit is contained in:
638
src/Output.php
Normal file
638
src/Output.php
Normal file
@@ -0,0 +1,638 @@
|
||||
<?php
|
||||
|
||||
namespace dokuwiki\plugin\luxtools;
|
||||
|
||||
class Output
|
||||
{
|
||||
/** @var \Doku_Renderer */
|
||||
protected $renderer;
|
||||
|
||||
/** @var string */
|
||||
protected $basedir;
|
||||
|
||||
/** @var string */
|
||||
protected $webdir;
|
||||
|
||||
/** @var array */
|
||||
protected $files;
|
||||
|
||||
/** @var Path|false|null */
|
||||
protected $openPathMapper = null;
|
||||
|
||||
|
||||
public function __construct(\Doku_Renderer $renderer, $basedir, $webdir, $files)
|
||||
{
|
||||
$this->renderer = $renderer;
|
||||
$this->basedir = $basedir;
|
||||
$this->webdir = $webdir;
|
||||
$this->files = $files;
|
||||
}
|
||||
|
||||
public function renderAsList($params)
|
||||
{
|
||||
$this->openContainer($params);
|
||||
|
||||
$this->renderListItems($this->files, $params);
|
||||
|
||||
$this->closeContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a thumbnail gallery (XHTML only).
|
||||
*
|
||||
* Expects a flat list of file items in $this->files.
|
||||
* Clicking a thumbnail opens the original image.
|
||||
*
|
||||
* @param array $params
|
||||
* @return void
|
||||
*/
|
||||
public function renderAsGallery($params)
|
||||
{
|
||||
if (!($this->renderer instanceof \Doku_Renderer_xhtml)) {
|
||||
$params['style'] = 'list';
|
||||
$this->renderAsList($params);
|
||||
return;
|
||||
}
|
||||
|
||||
$thumbW = 150;
|
||||
$thumbH = 150;
|
||||
$thumbQ = 80;
|
||||
|
||||
// Allow generating larger thumbnails (e.g. 2x) while still displaying them
|
||||
// at 150x150 for sharper results on HiDPI screens.
|
||||
$thumbScale = 1.0;
|
||||
$syntax = plugin_load('syntax', 'luxtools');
|
||||
if ($syntax) {
|
||||
$rawScale = (string)$syntax->getConf('gallery_thumb_scale');
|
||||
if ($rawScale !== '') {
|
||||
$thumbScale = (float)$rawScale;
|
||||
}
|
||||
}
|
||||
if (!is_finite($thumbScale) || $thumbScale < 1.0) $thumbScale = 1.0;
|
||||
if ($thumbScale > 4.0) $thumbScale = 4.0;
|
||||
|
||||
$genThumbW = (int)max(1, (int)round($thumbW * $thumbScale));
|
||||
$genThumbH = (int)max(1, (int)round($thumbH * $thumbScale));
|
||||
|
||||
$placeholderUrl = DOKU_BASE . 'lib/images/blank.gif';
|
||||
$placeholderId = $syntax ? trim((string)$syntax->getConf('thumb_placeholder')) : '';
|
||||
if ($placeholderId !== '' && function_exists('ml')) {
|
||||
// ml() builds a fetch.php URL for a MediaManager item
|
||||
$placeholderUrl = ml($placeholderId, ['w' => $genThumbW, 'h' => $genThumbH], true, '&');
|
||||
}
|
||||
|
||||
/** @var \Doku_Renderer_xhtml $renderer */
|
||||
$renderer = $this->renderer;
|
||||
$renderer->doc .= '<div class="luxtools-plugin luxtools-gallery" data-luxtools-gallery="1">';
|
||||
|
||||
foreach ($this->files as $item) {
|
||||
$url = $this->itemWebUrl($item, !empty($params['randlinks']));
|
||||
$thumbUrl = $this->withQueryParams($url, [
|
||||
'thumb' => 1,
|
||||
'w' => $genThumbW,
|
||||
'h' => $genThumbH,
|
||||
// Keep quality explicit so cache file naming stays stable.
|
||||
'q' => $thumbQ,
|
||||
]);
|
||||
$safeUrl = hsc($url);
|
||||
$safeThumbUrl = hsc($thumbUrl);
|
||||
$safePlaceholderUrl = hsc($placeholderUrl);
|
||||
$label = hsc($item['name']);
|
||||
$caption = hsc(basename((string)($item['name'] ?? '')));
|
||||
if ($caption === '') {
|
||||
$caption = $label;
|
||||
}
|
||||
|
||||
$initialSrc = $safePlaceholderUrl;
|
||||
$dataThumb = ' data-thumb-src="' . $safeThumbUrl . '"';
|
||||
$thumbCachePath = $this->thumbCachePathForItem($item, $genThumbW, $genThumbH, $thumbQ);
|
||||
if (is_string($thumbCachePath) && $thumbCachePath !== '' && @is_file($thumbCachePath)) {
|
||||
// Thumb already exists: start with it immediately (no JS swap needed)
|
||||
$initialSrc = $safeThumbUrl;
|
||||
$dataThumb = '';
|
||||
}
|
||||
|
||||
$renderer->doc .= '<a'
|
||||
. ' href="' . $safeUrl . '"'
|
||||
. ' class="media luxtools-gallery-item"'
|
||||
. ' title="' . $label . '"'
|
||||
. ' aria-label="' . $label . '"'
|
||||
. ' data-luxtools-full="' . $safeUrl . '"'
|
||||
. ' data-luxtools-name="' . $caption . '"'
|
||||
. '>';
|
||||
$renderer->doc .= '<img'
|
||||
. ' class="luxtools-thumb"'
|
||||
. ' src="' . $initialSrc . '"'
|
||||
. $dataThumb
|
||||
. ' alt=""'
|
||||
. ' width="' . $thumbW . '"'
|
||||
. ' height="' . $thumbH . '"'
|
||||
. ' loading="lazy"'
|
||||
. ' decoding="async"'
|
||||
. ' />';
|
||||
$renderer->doc .= '<span class="luxtools-gallery-caption">' . $caption . '</span>';
|
||||
$renderer->doc .= '</a>';
|
||||
}
|
||||
|
||||
$renderer->doc .= '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Append query parameters to a URL.
|
||||
*
|
||||
* Preserves existing query and fragment and uses RFC3986 encoding.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
protected function withQueryParams(string $url, array $params): string
|
||||
{
|
||||
if ($params === []) return $url;
|
||||
|
||||
$fragment = '';
|
||||
$hashPos = strpos($url, '#');
|
||||
if ($hashPos !== false) {
|
||||
$fragment = substr($url, $hashPos);
|
||||
$url = substr($url, 0, $hashPos);
|
||||
}
|
||||
|
||||
$glue = (strpos($url, '?') === false) ? '?' : '&';
|
||||
return $url . $glue . http_build_query($params, '', '&', PHP_QUERY_RFC3986) . $fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the expected thumbnail cache path for an item.
|
||||
*
|
||||
* Mirrors the hashing scheme in file.php so we can detect whether a thumb
|
||||
* already exists and can be used immediately.
|
||||
*
|
||||
* @param array $item
|
||||
* @param int $w
|
||||
* @param int $h
|
||||
* @param int $q
|
||||
* @return string|null
|
||||
*/
|
||||
protected function thumbCachePathForItem(array $item, int $w, int $h, int $q): ?string
|
||||
{
|
||||
if (!isset($item['path']) || !is_string($item['path']) || $item['path'] === '') return null;
|
||||
if (!isset($item['mtime'])) return null;
|
||||
|
||||
$path = $item['path'];
|
||||
$mtime = (int)$item['mtime'];
|
||||
|
||||
// Decide output format the same way file.php does.
|
||||
try {
|
||||
[, $mime,] = mimetype($path, false);
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
if (!is_string($mime) || !str_starts_with($mime, 'image/')) return null;
|
||||
$dstFormat = ($mime === 'image/png' || $mime === 'image/gif') ? 'png' : 'jpg';
|
||||
|
||||
global $conf;
|
||||
if (!isset($conf['cachedir']) || !is_string($conf['cachedir']) || trim($conf['cachedir']) === '') return null;
|
||||
|
||||
$hash = sha1($path . '|' . $mtime . '|w=' . $w . '|h=' . $h . '|q=' . $q . '|f=' . $dstFormat);
|
||||
$sub = substr($hash, 0, 2);
|
||||
$cacheDir = rtrim($conf['cachedir'], '/');
|
||||
return $cacheDir . '/luxtools/thumbs/' . $sub . '/' . $hash . '.' . $dstFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the files as a table, including details if configured that way.
|
||||
*
|
||||
* @param array $params the parameters of the filelist call
|
||||
*/
|
||||
public function renderAsTable($params)
|
||||
{
|
||||
$this->openContainer($params);
|
||||
|
||||
$items = $this->flattenResultTree($this->files);
|
||||
$this->renderTableItems($items, $params);
|
||||
|
||||
$this->closeContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a flat list (files and/or directories) as a table.
|
||||
*
|
||||
* @param array $params
|
||||
* @return void
|
||||
*/
|
||||
public function renderAsFlatTable($params)
|
||||
{
|
||||
$this->openContainer($params);
|
||||
|
||||
$this->renderTableItems($this->files, $params);
|
||||
|
||||
$this->closeContainer();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Open the wrapping container with an optional max-height and scroll behaviour.
|
||||
*/
|
||||
protected function openContainer($params): void
|
||||
{
|
||||
if (!($this->renderer instanceof \Doku_Renderer_xhtml)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$style = $this->containerStyle($params);
|
||||
$this->renderer->doc .= '<div class="luxtools-plugin"' . $style . '>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the wrapping container if XHTML renderer is in use.
|
||||
*/
|
||||
protected function closeContainer(): void
|
||||
{
|
||||
if (!($this->renderer instanceof \Doku_Renderer_xhtml)) {
|
||||
return;
|
||||
}
|
||||
$this->renderer->doc .= '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the inline style attribute for the container based on the maxheight param.
|
||||
*/
|
||||
protected function containerStyle($params): string
|
||||
{
|
||||
if (!isset($params['maxheight'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$maxHeight = (int)$params['maxheight'];
|
||||
if ($maxHeight < 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ' style="max-height: ' . $maxHeight . 'px; overflow-y: auto;"';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders the files as a table, including details if configured that way.
|
||||
*
|
||||
* @param array $params the parameters of the filelist call
|
||||
*/
|
||||
protected function renderTableItems($items, $params)
|
||||
{
|
||||
|
||||
$renderer = $this->renderer;
|
||||
|
||||
|
||||
// count the columns
|
||||
$columns = 1;
|
||||
if ($params['showsize']) {
|
||||
$columns++;
|
||||
}
|
||||
if ($params['showdate']) {
|
||||
$columns++;
|
||||
}
|
||||
|
||||
$renderer->table_open($columns);
|
||||
|
||||
$hasOpenLocation = isset($params['openlocation']) && is_string($params['openlocation']) && trim($params['openlocation']) !== '';
|
||||
$hasHeader = !empty($params['tableheader']);
|
||||
if ($hasOpenLocation || $hasHeader) {
|
||||
$renderer->tablethead_open();
|
||||
|
||||
// Small row above the header with an "Open Location" link.
|
||||
if ($hasOpenLocation && ($renderer instanceof \Doku_Renderer_xhtml)) {
|
||||
$openItem = [
|
||||
'name' => $this->getLang('openlocation'),
|
||||
'path' => $params['openlocation'],
|
||||
'isdir' => true,
|
||||
// Render without an icon (no mf_* class).
|
||||
'noicon' => true,
|
||||
];
|
||||
|
||||
/** @var \Doku_Renderer_xhtml $renderer */
|
||||
$renderer->doc .= '<tr class="luxtools-openlocation-row"><td colspan="' . (int)$columns . '">';
|
||||
$this->renderDirectoryLink($openItem);
|
||||
$renderer->doc .= '</td></tr>';
|
||||
}
|
||||
|
||||
if ($hasHeader) {
|
||||
$renderer->tablerow_open();
|
||||
|
||||
$renderer->tableheader_open();
|
||||
$renderer->cdata($this->getLang('filename'));
|
||||
$renderer->tableheader_close();
|
||||
|
||||
if ($params['showsize']) {
|
||||
$renderer->tableheader_open();
|
||||
$renderer->cdata($this->getLang('filesize'));
|
||||
$renderer->tableheader_close();
|
||||
}
|
||||
|
||||
if ($params['showdate']) {
|
||||
$renderer->tableheader_open();
|
||||
$renderer->cdata($this->getLang('lastmodified'));
|
||||
$renderer->tableheader_close();
|
||||
}
|
||||
|
||||
$renderer->tablerow_close();
|
||||
}
|
||||
|
||||
$renderer->tablethead_close();
|
||||
}
|
||||
|
||||
$renderer->tabletbody_open();
|
||||
foreach ($items as $item) {
|
||||
$renderer->tablerow_open();
|
||||
$renderer->tablecell_open();
|
||||
$this->renderItemLink($item, $params['randlinks']);
|
||||
$renderer->tablecell_close();
|
||||
|
||||
if ($params['showsize']) {
|
||||
$renderer->tablecell_open(1, 'right');
|
||||
if (!empty($item['isdir'])) {
|
||||
$renderer->cdata('');
|
||||
} else {
|
||||
$renderer->cdata(filesize_h($item['size']));
|
||||
}
|
||||
$renderer->tablecell_close();
|
||||
}
|
||||
|
||||
if ($params['showdate']) {
|
||||
$renderer->tablecell_open();
|
||||
$renderer->cdata(dformat($item['mtime']));
|
||||
$renderer->tablecell_close();
|
||||
}
|
||||
|
||||
$renderer->tablerow_close();
|
||||
}
|
||||
$renderer->tabletbody_close();
|
||||
$renderer->table_close();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively renders a tree of files as list items.
|
||||
*
|
||||
* @param array $items the files to render
|
||||
* @param array $params the parameters of the filelist call
|
||||
* @param int $level the level to render
|
||||
* @return void
|
||||
*/
|
||||
protected function renderListItems($items, $params, $level = 1)
|
||||
{
|
||||
if ($params['style'] == 'olist') {
|
||||
$this->renderer->listo_open();
|
||||
} else {
|
||||
$this->renderer->listu_open();
|
||||
}
|
||||
|
||||
foreach ($items as $file) {
|
||||
if ($file['children'] === false && $file['treesize'] === 0) continue; // empty directory
|
||||
|
||||
$this->renderer->listitem_open($level);
|
||||
$this->renderer->listcontent_open();
|
||||
|
||||
if ($file['children'] !== false && $file['treesize'] > 0) {
|
||||
// render the directory and its subtree
|
||||
$this->renderer->cdata($file['name']);
|
||||
$this->renderListItems($file['children'], $params, $level + 1);
|
||||
} elseif ($file['children'] === false) {
|
||||
// render the file link
|
||||
$this->renderItemLink($file, $params['randlinks']);
|
||||
|
||||
// render filesize
|
||||
if ($params['showsize']) {
|
||||
$this->renderer->cdata($params['listsep'] . filesize_h($file['size']));
|
||||
}
|
||||
// render lastmodified
|
||||
if ($params['showdate']) {
|
||||
$this->renderer->cdata($params['listsep'] . dformat($file['mtime']));
|
||||
}
|
||||
}
|
||||
|
||||
$this->renderer->listcontent_close();
|
||||
$this->renderer->listitem_close();
|
||||
}
|
||||
|
||||
if ($params['style'] == 'olist') {
|
||||
$this->renderer->listo_close();
|
||||
} else {
|
||||
$this->renderer->listu_close();
|
||||
}
|
||||
}
|
||||
|
||||
protected function renderItemLink($item, $cachebuster = false)
|
||||
{
|
||||
if (!empty($item['isdir'])) {
|
||||
$this->renderDirectoryLink($item);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->renderer instanceof \Doku_Renderer_xhtml) {
|
||||
$this->renderItemLinkXHTML($item, $cachebuster);
|
||||
} else {
|
||||
$this->renderItemLinkAny($item, $cachebuster);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a directory like a normal media link, but with open behaviour.
|
||||
*
|
||||
* @param array $item
|
||||
* @return void
|
||||
*/
|
||||
protected function renderDirectoryLink($item)
|
||||
{
|
||||
$caption = $item['name'] ?? '';
|
||||
$path = $item['path'] ?? '';
|
||||
|
||||
if ($caption === '') {
|
||||
$caption = '[n/a]';
|
||||
}
|
||||
|
||||
if (!($this->renderer instanceof \Doku_Renderer_xhtml)) {
|
||||
$this->renderer->cdata($caption);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_string($path) || $path === '') {
|
||||
$this->renderer->cdata('[n/a]');
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $this->mapOpenPath($path);
|
||||
|
||||
global $conf;
|
||||
/** @var \Doku_Renderer_xhtml $renderer */
|
||||
$renderer = $this->renderer;
|
||||
|
||||
$syntax = plugin_load('syntax', 'luxtools');
|
||||
$serviceUrl = $syntax ? trim((string)$syntax->getConf('open_service_url')) : '';
|
||||
|
||||
// Prepare a DokuWiki-style link.
|
||||
// Use the same icon mechanism as normal media links (via $link['class']).
|
||||
$link = [
|
||||
'target' => $conf['target']['extern'],
|
||||
'style' => '',
|
||||
'pre' => '',
|
||||
'suf' => '',
|
||||
'name' => $caption,
|
||||
'url' => '#',
|
||||
'title' => $renderer->_xmlEntities($path),
|
||||
'more' => '',
|
||||
];
|
||||
|
||||
$noIcon = !empty($item['noicon']);
|
||||
$link['class'] = $noIcon
|
||||
? 'luxtools-open'
|
||||
: 'luxtools-open media mediafile mf_folder';
|
||||
|
||||
$link['more'] .= ' data-path="' . hsc($path) . '"';
|
||||
if ($conf['relnofollow']) $link['more'] .= ' rel="nofollow"';
|
||||
if ($serviceUrl !== '') $link['more'] .= ' data-service-url="' . hsc($serviceUrl) . '"';
|
||||
$renderer->doc .= $renderer->_formatLink($link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a filesystem path to an alias path (if configured).
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
protected function mapOpenPath($path)
|
||||
{
|
||||
if ($this->openPathMapper === false) return $path;
|
||||
|
||||
if ($this->openPathMapper === null) {
|
||||
$syntax = plugin_load('syntax', 'luxtools');
|
||||
$pathConfig = $syntax ? (string)$syntax->getConf('paths') : '';
|
||||
|
||||
if (trim($pathConfig) === '') {
|
||||
$this->openPathMapper = false;
|
||||
return $path;
|
||||
}
|
||||
|
||||
$this->openPathMapper = new Path($pathConfig);
|
||||
}
|
||||
|
||||
return $this->openPathMapper->mapToAliasPath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a file link on the XHTML renderer
|
||||
*/
|
||||
protected function renderItemLinkXHTML($item, $cachebuster = false)
|
||||
{
|
||||
global $conf;
|
||||
/** @var \Doku_Renderer_xhtml $renderer */
|
||||
$renderer = $this->renderer;
|
||||
|
||||
//prepare for formating
|
||||
$link['target'] = $conf['target']['extern'];
|
||||
$link['style'] = '';
|
||||
$link['pre'] = '';
|
||||
$link['suf'] = '';
|
||||
$link['more'] = '';
|
||||
$link['url'] = $this->itemWebUrl($item, $cachebuster);
|
||||
$link['name'] = $item['name'];
|
||||
$link['title'] = $renderer->_xmlEntities($link['url']);
|
||||
if ($conf['relnofollow']) $link['more'] .= ' rel="nofollow"';
|
||||
[$ext,] = mimetype(basename($item['local']));
|
||||
$link['class'] = 'media mediafile mf_' . $ext;
|
||||
$renderer->doc .= $renderer->_formatLink($link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a file link on any Renderer
|
||||
* @param array $item
|
||||
* @param bool $cachebuster
|
||||
* @return void
|
||||
*/
|
||||
protected function renderItemLinkAny($item, $cachebuster = false)
|
||||
{
|
||||
$this->renderer->externalmedialink($this->itemWebUrl($item, $cachebuster), $item['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the Web URL for a given item
|
||||
*
|
||||
* @param array $item The item data as returned by the Crawler
|
||||
* @param bool $cachbuster add a cachebuster to the URL?
|
||||
* @return string
|
||||
*/
|
||||
protected function itemWebUrl($item, $cachbuster = false)
|
||||
{
|
||||
$webdir = $this->webdir;
|
||||
|
||||
// When using the built-in file-serving endpoint, include the current page id
|
||||
// so file.php can enforce DokuWiki ACLs for that page.
|
||||
if (
|
||||
is_string($webdir)
|
||||
&& $webdir !== ''
|
||||
&& strpos($webdir, 'lib/plugins/luxtools/file.php') !== false
|
||||
&& strpos($webdir, 'id=') === false
|
||||
) {
|
||||
global $ID;
|
||||
$pageId = isset($ID) ? (string)$ID : '';
|
||||
if ($pageId !== '') {
|
||||
if (function_exists('cleanID')) {
|
||||
$pageId = (string)cleanID($pageId);
|
||||
}
|
||||
if ($pageId !== '') {
|
||||
$encoded = rawurlencode($pageId);
|
||||
if (strpos($webdir, '&file=') !== false) {
|
||||
$webdir = str_replace('&file=', '&id=' . $encoded . '&file=', $webdir);
|
||||
} elseif (strpos($webdir, '?file=') !== false) {
|
||||
$webdir = str_replace('?file=', '?id=' . $encoded . '&file=', $webdir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (str_ends_with($webdir, '=')) {
|
||||
$url = $webdir . rawurlencode($item['local']);
|
||||
} else {
|
||||
$url = $webdir . $item['local'];
|
||||
}
|
||||
|
||||
if ($cachbuster) {
|
||||
if (strpos($url, '?') === false) {
|
||||
$url .= '?t=' . $item['mtime'];
|
||||
} else {
|
||||
$url .= '&t=' . $item['mtime'];
|
||||
}
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the filelist by recursively walking through all subtrees and
|
||||
* merging them with a prefix attached to the filenames.
|
||||
*
|
||||
* @param array $items the tree to flatten
|
||||
* @param string $prefix the prefix to attach to all processed nodes
|
||||
* @return array a flattened representation of the tree
|
||||
*/
|
||||
protected function flattenResultTree($items, $prefix = '')
|
||||
{
|
||||
$result = [];
|
||||
foreach ($items as $file) {
|
||||
if ($file['children'] !== false) {
|
||||
$result = array_merge(
|
||||
$result,
|
||||
$this->flattenResultTree($file['children'], $prefix . $file['name'] . '/')
|
||||
);
|
||||
} else {
|
||||
$file['name'] = $prefix . $file['name'];
|
||||
$result[] = $file;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function getLang($key)
|
||||
{
|
||||
$syntax = plugin_load('syntax', 'luxtools');
|
||||
return $syntax->getLang($key);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user