mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-23 22:10:55 +01:00
Allow simple template-based skin definitions
Summary: Lower the barrier to entry for installing and creating skins, so we can kill Wordpress. You can now install skins by dropping them into a directory, and build either "advanced" (full phutil library) skins or "basic" (simple PHP templates) skins. Next up is getting static resources working in an easy way for skins. I put these in `externals/` for now so they don't get hit by lint. Test Plan: Viewed the Pokeblog with the Oblivious skin. Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T1373 Differential Revision: https://secure.phabricator.com/D3717
This commit is contained in:
parent
44c6109bf2
commit
b3ad8507af
17 changed files with 384 additions and 64 deletions
|
@ -1084,6 +1084,11 @@ return array(
|
|||
// Information on shortnames - http://docs.disqus.com/help/68/
|
||||
'disqus.shortname' => null,
|
||||
|
||||
// Directories to look for Phame skins inside of.
|
||||
'phame.skins' => array(
|
||||
'externals/skin/',
|
||||
),
|
||||
|
||||
// -- Remarkup -------------------------------------------------------------- //
|
||||
|
||||
// If you enable this, linked YouTube videos will be embeded inline. This has
|
||||
|
|
1
externals/skins/oblivious/404.php
vendored
Normal file
1
externals/skins/oblivious/404.php
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<h2>404 Not Found</h2>
|
3
externals/skins/oblivious/footer.php
vendored
Normal file
3
externals/skins/oblivious/footer.php
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
</div>
|
||||
</body>
|
||||
</html>
|
15
externals/skins/oblivious/header.php
vendored
Normal file
15
externals/skins/oblivious/header.php
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><?php echo _e($title); ?></title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="oblivious-info">
|
||||
<h1>
|
||||
<a href="<?php echo _e($uri); ?>"><?php
|
||||
echo _e($blog->getName());
|
||||
?></a>
|
||||
</h1>
|
||||
<p><?php echo _e($blog->getDescription()); ?></p>
|
||||
</div>
|
||||
<div class="oblivious-content">
|
1
externals/skins/oblivious/post-detail.php
vendored
Normal file
1
externals/skins/oblivious/post-detail.php
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<?php echo $post->render(); ?>
|
13
externals/skins/oblivious/post-list.php
vendored
Normal file
13
externals/skins/oblivious/post-list.php
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div class="oblivious-post-list">
|
||||
<?php
|
||||
|
||||
foreach ($posts as $post) {
|
||||
echo $post->renderWithSummary();
|
||||
}
|
||||
|
||||
?>
|
||||
</div>
|
||||
<div class="oblivious-pager">
|
||||
<?php echo $older; ?>
|
||||
<?php echo $newer; ?>
|
||||
</div>
|
3
externals/skins/oblivious/skin.json
vendored
Normal file
3
externals/skins/oblivious/skin.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"name": "Oblivious"
|
||||
}
|
|
@ -1150,6 +1150,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorXHProfSampleListController' => 'applications/xhprof/controller/PhabricatorXHProfSampleListController.php',
|
||||
'PhabricatorXHProfSampleListView' => 'applications/xhprof/view/PhabricatorXHProfSampleListView.php',
|
||||
'PhameBasicBlogSkin' => 'applications/phame/skins/PhameBasicBlogSkin.php',
|
||||
'PhameBasicTemplateBlogSkin' => 'applications/phame/skins/PhameBasicTemplateBlogSkin.php',
|
||||
'PhameBlog' => 'applications/phame/storage/PhameBlog.php',
|
||||
'PhameBlogDeleteController' => 'applications/phame/controller/blog/PhameBlogDeleteController.php',
|
||||
'PhameBlogEditController' => 'applications/phame/controller/blog/PhameBlogEditController.php',
|
||||
|
@ -1157,7 +1158,6 @@ phutil_register_library_map(array(
|
|||
'PhameBlogLiveController' => 'applications/phame/controller/blog/PhameBlogLiveController.php',
|
||||
'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php',
|
||||
'PhameBlogSkin' => 'applications/phame/skins/PhameBlogSkin.php',
|
||||
'PhameBlogSkinTenEleven' => 'applications/phame/skins/PhameBlogSkinTenEleven.php',
|
||||
'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php',
|
||||
'PhameController' => 'applications/phame/controller/PhameController.php',
|
||||
'PhameDAO' => 'applications/phame/storage/PhameDAO.php',
|
||||
|
@ -1174,6 +1174,7 @@ phutil_register_library_map(array(
|
|||
'PhamePostUnpublishController' => 'applications/phame/controller/post/PhamePostUnpublishController.php',
|
||||
'PhamePostView' => 'applications/phame/view/PhamePostView.php',
|
||||
'PhamePostViewController' => 'applications/phame/controller/post/PhamePostViewController.php',
|
||||
'PhameSkinSpecification' => 'applications/phame/skins/PhameSkinSpecification.php',
|
||||
'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
|
||||
'PhortuneStripeBaseController' => 'applications/phortune/stripe/controller/PhortuneStripeBaseController.php',
|
||||
'PhortuneStripePaymentFormView' => 'applications/phortune/stripe/view/PhortuneStripePaymentFormView.php',
|
||||
|
@ -2278,6 +2279,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorXHProfSampleListController' => 'PhabricatorXHProfController',
|
||||
'PhabricatorXHProfSampleListView' => 'AphrontView',
|
||||
'PhameBasicBlogSkin' => 'PhameBlogSkin',
|
||||
'PhameBasicTemplateBlogSkin' => 'PhameBasicBlogSkin',
|
||||
'PhameBlog' =>
|
||||
array(
|
||||
0 => 'PhameDAO',
|
||||
|
@ -2290,7 +2292,6 @@ phutil_register_library_map(array(
|
|||
'PhameBlogLiveController' => 'PhameController',
|
||||
'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhameBlogSkin' => 'PhabricatorController',
|
||||
'PhameBlogSkinTenEleven' => 'PhameBasicBlogSkin',
|
||||
'PhameBlogViewController' => 'PhameController',
|
||||
'PhameController' => 'PhabricatorController',
|
||||
'PhameDAO' => 'PhabricatorLiskDAO',
|
||||
|
|
|
@ -118,6 +118,9 @@ final class PhameBlogEditController
|
|||
->setObject($blog)
|
||||
->execute();
|
||||
|
||||
$skins = PhameSkinSpecification::loadAllSkinSpecifications();
|
||||
$skins = mpull($skins, 'getName');
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($user)
|
||||
->setFlexible(true)
|
||||
|
@ -171,7 +174,7 @@ final class PhameBlogEditController
|
|||
->setLabel('Skin')
|
||||
->setName('skin')
|
||||
->setValue($blog->getSkin())
|
||||
->setOptions(PhameBlog::getSkinOptionsForSelect())
|
||||
->setOptions($skins)
|
||||
)
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
|
|
|
@ -64,6 +64,7 @@ final class PhameBlogLiveController extends PhameController {
|
|||
->setBlog($blog)
|
||||
->setBaseURI($request->getRequestURI()->setPath($base_path));
|
||||
|
||||
$skin->willProcessRequest(array());
|
||||
return $skin->processRequest();
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ abstract class PhameBasicBlogSkin extends PhameBlogSkin {
|
|||
|
||||
$response = new AphrontWebpageResponse();
|
||||
$response->setContent($view->render());
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
@ -127,6 +128,9 @@ abstract class PhameBasicBlogSkin extends PhameBlogSkin {
|
|||
*/
|
||||
protected function renderOlderPageLink() {
|
||||
$uri = $this->getOlderPageURI();
|
||||
if (!$uri) {
|
||||
return null;
|
||||
}
|
||||
return phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
|
@ -156,6 +160,9 @@ abstract class PhameBasicBlogSkin extends PhameBlogSkin {
|
|||
*/
|
||||
protected function renderNewerPageLink() {
|
||||
$uri = $this->getNewerPageURI();
|
||||
if (!$uri) {
|
||||
return null;
|
||||
}
|
||||
return phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
|
|
97
src/applications/phame/skins/PhameBasicTemplateBlogSkin.php
Normal file
97
src/applications/phame/skins/PhameBasicTemplateBlogSkin.php
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @group phame
|
||||
*/
|
||||
final class PhameBasicTemplateBlogSkin extends PhameBasicBlogSkin {
|
||||
|
||||
public function willProcessRequest(array $data) {
|
||||
$root = dirname(phutil_get_library_root('phabricator'));
|
||||
require_once $root.'/support/phame/libskin.php';
|
||||
|
||||
parent::willProcessRequest($data);
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return $this->getSpecification()->getName();
|
||||
}
|
||||
|
||||
public function getPath($to_file = null) {
|
||||
$path = $this->getSpecification()->getRootDirectory();
|
||||
if ($to_file) {
|
||||
$path = $path.DIRECTORY_SEPARATOR.$to_file;
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function renderTemplate($__template__, array $__scope__) {
|
||||
chdir($this->getPath());
|
||||
ob_start();
|
||||
|
||||
if (Filesystem::pathExists($this->getPath($__template__))) {
|
||||
extract($__scope__ + $this->getDefaultScope());
|
||||
require $this->getPath($__template__);
|
||||
}
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
private function getDefaultScope() {
|
||||
return array(
|
||||
'skin' => $this,
|
||||
'blog' => $this->getBlog(),
|
||||
'uri' => $this->getURI(''),
|
||||
);
|
||||
}
|
||||
|
||||
protected function renderHeader() {
|
||||
return $this->renderTemplate(
|
||||
'header.php',
|
||||
array(
|
||||
'title' => $this->getBlog()->getName(),
|
||||
));
|
||||
}
|
||||
|
||||
protected function renderFooter() {
|
||||
return $this->renderTemplate('footer.php', array());
|
||||
}
|
||||
|
||||
protected function render404Page() {
|
||||
return $this->renderTemplate('404.php', array());
|
||||
}
|
||||
|
||||
protected function renderPostDetail(PhamePostView $post) {
|
||||
return $this->renderTemplate(
|
||||
'post-detail.php',
|
||||
array(
|
||||
'post' => $post,
|
||||
));
|
||||
}
|
||||
|
||||
protected function renderPostList(array $posts) {
|
||||
return $this->renderTemplate(
|
||||
'post-list.php',
|
||||
array(
|
||||
'posts' => $posts,
|
||||
'older' => $this->renderNewerPageLink(),
|
||||
'newer' => $this->renderOlderPageLink(),
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -24,6 +24,16 @@ abstract class PhameBlogSkin extends PhabricatorController {
|
|||
private $blog;
|
||||
private $baseURI;
|
||||
private $preview;
|
||||
private $specification;
|
||||
|
||||
public function setSpecification(PhameSkinSpecification $specification) {
|
||||
$this->specification = $specification;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSpecification() {
|
||||
return $this->specification;
|
||||
}
|
||||
|
||||
public function setPreview($preview) {
|
||||
$this->preview = $preview;
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @group phame
|
||||
*/
|
||||
final class PhameBlogSkinTenEleven extends PhameBasicBlogSkin {
|
||||
|
||||
public function getName() {
|
||||
return 'Ten Eleven (Default)';
|
||||
}
|
||||
|
||||
protected function renderHeader() {
|
||||
$blog_name = $this->getBlog()->getName();
|
||||
$about = $this->getBlog()->getDescription();
|
||||
|
||||
$about = phutil_escape_html($about);
|
||||
|
||||
return <<<EOHTML
|
||||
<div class="main-panel">
|
||||
<div class="header">
|
||||
<h1>{$blog_name}</h1>
|
||||
<div class="about">{$about}</div>
|
||||
</div>
|
||||
<div class="splash">
|
||||
|
||||
</div>
|
||||
<div class="content">
|
||||
EOHTML;
|
||||
}
|
||||
|
||||
protected function renderFooter() {
|
||||
return <<<EOHTML
|
||||
</div>
|
||||
</div>
|
||||
EOHTML;
|
||||
}
|
||||
|
||||
}
|
190
src/applications/phame/skins/PhameSkinSpecification.php
Normal file
190
src/applications/phame/skins/PhameSkinSpecification.php
Normal file
|
@ -0,0 +1,190 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @group phame
|
||||
*/
|
||||
final class PhameSkinSpecification {
|
||||
|
||||
const TYPE_ADVANCED = 'advanced';
|
||||
const TYPE_BASIC = 'basic';
|
||||
|
||||
private $type;
|
||||
private $rootDirectory;
|
||||
private $skinClass;
|
||||
private $phutilLibraries = array();
|
||||
private $name;
|
||||
private $config;
|
||||
|
||||
public static function loadAllSkinSpecifications() {
|
||||
|
||||
static $specs;
|
||||
|
||||
if ($specs === null) {
|
||||
$paths = PhabricatorEnv::getEnvConfig('phame.skins');
|
||||
$base = dirname(phutil_get_library_root('phabricator'));
|
||||
|
||||
$specs = array();
|
||||
|
||||
foreach ($paths as $path) {
|
||||
$path = Filesystem::resolvePath($path, $base);
|
||||
foreach (Filesystem::listDirectory($path) as $skin_directory) {
|
||||
$skin_path = $path.DIRECTORY_SEPARATOR.$skin_directory;
|
||||
|
||||
if (!is_dir($skin_path)) {
|
||||
continue;
|
||||
}
|
||||
$spec = self::loadSkinSpecification($skin_path);
|
||||
if (!$spec) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim($skin_directory, DIRECTORY_SEPARATOR);
|
||||
|
||||
$spec->setName($name);
|
||||
|
||||
if (isset($specs[$name])) {
|
||||
$that_dir = $specs[$name]->getRootDirectory();
|
||||
$this_dir = $spec->getRootDirectory();
|
||||
throw new Exception(
|
||||
"Two skins have the same name ('{$name}'), in '{$this_dir}' and ".
|
||||
"'{$that_dir}'. Rename one or adjust your 'phame.skins' ".
|
||||
"configuration.");
|
||||
}
|
||||
|
||||
$specs[$name] = $spec;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $specs;
|
||||
}
|
||||
|
||||
public static function loadOneSkinSpecification($name) {
|
||||
$paths = PhabricatorEnv::getEnvConfig('phame.skins');
|
||||
$base = dirname(phutil_get_library_root('phabricator'));
|
||||
foreach ($paths as $path) {
|
||||
$path = Filesystem::resolvePath($path, $base);
|
||||
$skin_path = $path.DIRECTORY_SEPARATOR.$name;
|
||||
if (is_dir($skin_path)) {
|
||||
$spec = self::loadSkinSpecification($skin_path);
|
||||
if ($spec) {
|
||||
$spec->setName($name);
|
||||
return $spec;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function loadSkinSpecification($path) {
|
||||
|
||||
$config_path = $path.DIRECTORY_SEPARATOR.'skin.json';
|
||||
$config = array();
|
||||
if (Filesystem::pathExists($config_path)) {
|
||||
$config = Filesystem::readFile($config_path);
|
||||
$config = json_decode($config, true);
|
||||
if (!is_array($config)) {
|
||||
throw new Exception(
|
||||
"Skin configuration file '{$config_path}' is not a valid JSON file.");
|
||||
}
|
||||
$type = idx($config, 'type', self::TYPE_BASIC);
|
||||
} else {
|
||||
$type = self::TYPE_BASIC;
|
||||
}
|
||||
|
||||
$spec = new PhameSkinSpecification();
|
||||
$spec->setRootDirectory($path);
|
||||
$spec->setConfig($config);
|
||||
|
||||
switch ($type) {
|
||||
case self::TYPE_BASIC:
|
||||
$spec->setSkinClass('PhameBasicTemplateBlogSkin');
|
||||
break;
|
||||
case self::TYPE_ADVANCED:
|
||||
$spec->setSkinClass($config['class']);
|
||||
$spec->addPhutilLibrary($path.DIRECTORY_SEPARATOR.'src');
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unknown skin type!");
|
||||
}
|
||||
|
||||
$spec->setType($type);
|
||||
|
||||
return $spec;
|
||||
}
|
||||
|
||||
public function setConfig(array $config) {
|
||||
$this->config = $config;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getConfig($key, $default = null) {
|
||||
return idx($this->config, $key, $default);
|
||||
}
|
||||
|
||||
public function setName($name) {
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return $this->getConfig('name', $this->name);
|
||||
}
|
||||
|
||||
public function setRootDirectory($root_directory) {
|
||||
$this->rootDirectory = $root_directory;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRootDirectory() {
|
||||
return $this->rootDirectory;
|
||||
}
|
||||
|
||||
public function setType($type) {
|
||||
$this->type = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType() {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setSkinClass($skin_class) {
|
||||
$this->skinClass = $skin_class;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSkinClass() {
|
||||
return $this->skinClass;
|
||||
}
|
||||
|
||||
public function addPhutilLibrary($library) {
|
||||
$this->phutilLibraries[] = $library;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function buildSkin(AphrontRequest $request) {
|
||||
foreach ($this->phutilLibraries as $library) {
|
||||
phutil_load_library($library);
|
||||
}
|
||||
|
||||
return newv($this->getSkinClass(), array($request, $this));
|
||||
}
|
||||
|
||||
}
|
|
@ -24,7 +24,7 @@ final class PhameBlog extends PhameDAO
|
|||
|
||||
const MARKUP_FIELD_DESCRIPTION = 'markup:description';
|
||||
|
||||
const SKIN_DEFAULT = 'PhameBlogSkinTenEleven';
|
||||
const SKIN_DEFAULT = 'oblivious';
|
||||
|
||||
protected $id;
|
||||
protected $phid;
|
||||
|
@ -57,14 +57,17 @@ final class PhameBlog extends PhameDAO
|
|||
}
|
||||
|
||||
public function getSkinRenderer(AphrontRequest $request) {
|
||||
try {
|
||||
$skin = newv($this->getSkin(), array($request));
|
||||
} catch (PhutilMissingSymbolException $ex) {
|
||||
// If this blog has a skin but it's no longer available (for example,
|
||||
// it was uninstalled) just return the default skin.
|
||||
$skin = newv(self::SKIN_DEFAULT, array($request));
|
||||
$spec = PhameSkinSpecification::loadOneSkinSpecification(
|
||||
$this->getSkin());
|
||||
|
||||
if (!$spec) {
|
||||
$spec = PhameSkinSpecification::loadOneSkinSpecification(
|
||||
self::SKIN_DEFAULT);
|
||||
}
|
||||
|
||||
$skin = newv($spec->getSkinClass(), array($request));
|
||||
$skin->setSpecification($spec);
|
||||
|
||||
return $skin;
|
||||
}
|
||||
|
||||
|
|
21
support/phame/libskin.php
Normal file
21
support/phame/libskin.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function _e($text) {
|
||||
return phutil_escape_html($text);
|
||||
}
|
Loading…
Reference in a new issue