Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions core/components/com_kb/models/article.php
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,86 @@ public function link($type='')
return $link;
}

/**
* A plain-text excerpt of the article around a search term
*
* The excerpt is HTML-escaped with each match wrapped in <mark>. When
* the whole phrase does not occur, the earliest of its words is used.
*
* @param string $needle Search term
* @param integer $radius Characters of context on either side
* @return string HTML, or '' when the term is empty or does not occur
*/
public function snippet($needle, $radius = 160)
{
$needle = trim(preg_replace('/\s+/u', ' ', (string) $needle));

if ($needle === '')
{
return '';
}

// Field wrappers hold editor markup, not prose
$text = preg_replace('#<nb:(\w+)>.*?</nb:\1>#is', ' ', stripslashes((string) $this->get('fulltxt')));
$text = html_entity_decode(strip_tags($text), ENT_QUOTES, 'UTF-8');
$text = trim(preg_replace('/\s+/u', ' ', $text));

if ($text === '')
{
return '';
}

// The phrase, else whichever of its words comes first
$terms = array($needle);
$position = mb_stripos($text, $needle, 0, 'UTF-8');

if ($position === false)
{
$terms = array();

foreach (array_unique(preg_split('/ /', $needle)) as $word)
{
$at = mb_stripos($text, $word, 0, 'UTF-8');

if ($at !== false)
{
$terms[] = $word;
$position = ($position === false) ? $at : min($position, $at);
}
}

if ($position === false)
{
return '';
}
}

$start = max(0, $position - $radius);
$length = $radius * 2 + mb_strlen($needle, 'UTF-8');
$excerpt = mb_substr($text, $start, $length, 'UTF-8');

// Cut on word boundaries, then mark the ends that were cut
if ($start > 0 && ($space = mb_strpos($excerpt, ' ', 0, 'UTF-8')) !== false)
{
$excerpt = mb_substr($excerpt, $space + 1, null, 'UTF-8');
}
if ($start + $length < mb_strlen($text, 'UTF-8') && ($space = mb_strrpos($excerpt, ' ', 0, 'UTF-8')) !== false)
{
$excerpt = mb_substr($excerpt, 0, $space, 'UTF-8');
}

$excerpt = htmlspecialchars($excerpt, ENT_QUOTES, 'UTF-8');

$patterns = array_map(function ($term)
{
return preg_quote(htmlspecialchars($term, ENT_QUOTES, 'UTF-8'), '/');
}, $terms);

$excerpt = preg_replace('/(' . implode('|', $patterns) . ')/iu', '<mark>$1</mark>', $excerpt);

return ($start > 0 ? '&hellip; ' : '') . $excerpt . ($start + $length < mb_strlen($text, 'UTF-8') ? ' &hellip;' : '');
}

/**
* Get a list of votes
*
Expand Down
4 changes: 3 additions & 1 deletion core/components/com_kb/models/category.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ public function parent()
public function link($type='')
{
$link = $this->_base;
$link .= '&section=' . $this->get('path');
// A category built in code (the "all" pseudo-category) has an alias
// but no stored path; without a section the router picks the wrong task
$link .= '&section=' . ($this->get('path') ?: $this->get('alias'));

// If it doesn't exist or isn't published
switch (strtolower($type))
Expand Down
19 changes: 19 additions & 0 deletions core/components/com_kb/site/assets/css/kb.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,22 @@
line-height: 0;
clear: left;
}
/* Search results */
.com_kb .entry-snippet {
display: block;
margin: 0.3em 0 0.5em 0;
color: #666;
font-size: 0.95em;
line-height: 1.4;
}
.com_kb .entry-snippet mark {
padding: 0 0.15em;
background-color: #fff3a0;
color: inherit;
}
.com_kb .search-results-for {
margin: 0 0 0.5em 0;
}
.com_kb .search-results-for a {
margin-left: 0.5em;
}
30 changes: 25 additions & 5 deletions core/components/com_kb/site/controllers/articles.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,36 @@ public function execute()
*/
public function displayTask()
{
// The same contract as categoryTask(), so the two layouts can share
// the category navigation and search form
$this->view
->set('archive', $this->archive)
->set('category', $this->allCategory())
->set('catid', 0)
->set('filters', array(
'sort' => '',
'search' => ''
))
->setLayout('display')
->display();
}

/**
* The pseudo-category that stands for every article
*
* @return Category
*/
private function allCategory()
{
$category = new Category;
$category->set('alias', 'all');
$category->set('title', Lang::txt('COM_KB_ALL_ARTICLES'));
$category->set('id', 0);
$category->set('published', 1);

return $category;
}

/**
* Displays a list of articles for a given category
*
Expand All @@ -74,11 +98,7 @@ public function categoryTask()

if ($categoryAlias == 'all')
{
$category = new Category;
$category->set('alias', 'all');
$category->set('title', Lang::txt('COM_KB_ALL_ARTICLES'));
$category->set('id', 0);
$category->set('published', 1);
$category = $this->allCategory();
}
else
{
Expand Down
2 changes: 2 additions & 0 deletions core/components/com_kb/site/language/en-GB/en-GB.com_kb.ini
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ COM_KB_SEARCH="Search"
COM_KB_SEARCH_LEGEND="Search for articles"
COM_KB_SEARCH_LABEL="Enter keyword or phrase"
COM_KB_SEARCH_PLACEHOLDER="Enter keyword or phrase"
COM_KB_SEARCH_RESULTS_FOR="Results for &quot;%s&quot;"
COM_KB_SEARCH_CLEAR="Clear search"

; Errors
COM_KB_USER_ALREADY_VOTED="User has already voted."
Expand Down
63 changes: 63 additions & 0 deletions core/components/com_kb/site/views/articles/tmpl/_categories.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php
/**
* @package hubzero-cms
* @copyright Copyright (c) 2005-2026 The Regents of the University of California.
* @license http://opensource.org/licenses/MIT MIT
*/

// No direct access
defined('_HZEXEC_') or die();

// Category navigation. Expects: option, archive, catid (the active top-level
// category id, 0 for all), category (the active category), filters (sort,
// search), which the links carry along so a reader keeps their search while
// narrowing by category.
$filters = array('state' => 1, 'access' => User::getAuthorisedViewLevels());
$categories = $this->archive->categories($filters);

$carry = '';
if (!empty($this->filters['sort']))
{
$carry .= '&sort=' . $this->escape($this->filters['sort']);
}
if (!empty($this->filters['search']))
{
$carry .= '&search=' . urlencode($this->filters['search']);
}
?>
<nav aria-label="<?php echo Lang::txt('COM_KB_CATEGORIES'); ?>">
<div class="container">
<h3><?php echo Lang::txt('COM_KB_CATEGORIES'); ?></h3>
<ul class="categories">
<li>
<a<?php if ($this->catid <= 0) { echo ' class="active" aria-current="page"'; } ?> href="<?php echo Route::url('index.php?option=' . $this->option . '&section=all' . $carry); ?>">
<?php echo Lang::txt('COM_KB_ALL_ARTICLES'); ?>
</a>
</li>
<?php foreach ($categories as $row) { ?>
<?php
if ($row->get('articles', 0) <= 0)
{
continue;
}
?>
<li>
<a <?php if ($this->catid == $row->get('id')) { echo 'class="active" aria-current="page" '; } ?> href="<?php echo Route::url($row->link() . $carry); ?>">
<?php echo $this->escape(stripslashes($row->get('title'))); ?> <span class="item-count"><?php echo $row->get('articles', 0); ?></span>
</a>
<?php if ($this->catid == $row->get('id') && $row->children($filters)->total() > 0) { ?>
<ul class="categories">
<?php foreach ($row->children() as $cat) { ?>
<li>
<a <?php if ($this->category->get('id') == $cat->get('id')) { echo 'class="active" aria-current="page" '; } ?> href="<?php echo Route::url($cat->link() . $carry); ?>">
<?php echo $this->escape(stripslashes($cat->get('title'))); ?> <span class="item-count"><?php echo $cat->get('articles', 0); ?></span>
</a>
</li>
<?php } ?>
</ul>
<?php } ?>
</li>
<?php } ?>
</ul>
</div><!-- / .container -->
</nav>
37 changes: 37 additions & 0 deletions core/components/com_kb/site/views/articles/tmpl/_help.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/**
* @package hubzero-cms
* @copyright Copyright (c) 2005-2026 The Regents of the University of California.
* @license http://opensource.org/licenses/MIT MIT
*/

// No direct access
defined('_HZEXEC_') or die();

// Where to turn when the knowledge base does not answer the question. A
// template that routes help elsewhere overrides this file alone.
?>
<?php if (Component::isEnabled('com_answers')) { ?>
<div class="container">
<h3><?php echo Lang::txt('COM_KB_COMMUNITY'); ?></h3>
<p>
<?php echo Lang::txt('COM_KB_COMMUNITY_CANT_FIND'); ?> <?php echo Lang::txt('COM_KB_COMMUNITY_TRY_ANSWERS', '<a href="' . Route::url('index.php?option=com_answers') . '">' . Lang::txt('COM_ANSWERS') . '</a>'); ?>
</p>
</div><!-- / .container -->
<?php } ?>
<?php if (Component::isEnabled('com_wishlist')) { ?>
<div class="container">
<h3><?php echo Lang::txt('COM_KB_FEATURE_REQUEST'); ?></h3>
<p>
<?php echo Lang::txt('COM_KB_HAVE_A_FEATURE_REQUEST'); ?> <a href="<?php echo Route::url('index.php?option=com_wishlist'); ?>"><?php echo Lang::txt('COM_KB_FEATURE_TELL_US'); ?></a>
</p>
</div><!-- / .container -->
<?php } ?>
<?php if (Component::isEnabled('com_support')) { ?>
<div class="container">
<h3><?php echo Lang::txt('COM_KB_TROUBLE_REPORT'); ?></h3>
<p>
<?php echo Lang::txt('COM_KB_TROUBLE_FOUND_BUG'); ?> <a href="<?php echo Route::url('index.php?option=com_support&controller=tickets&task=new'); ?>"><?php echo Lang::txt('COM_KB_TROUBLE_TELL_US'); ?></a>
</p>
</div><!-- / .container -->
<?php } ?>
27 changes: 27 additions & 0 deletions core/components/com_kb/site/views/articles/tmpl/_search.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
* @package hubzero-cms
* @copyright Copyright (c) 2005-2026 The Regents of the University of California.
* @license http://opensource.org/licenses/MIT MIT
*/

// No direct access
defined('_HZEXEC_') or die();

// The article search form. Expects: option, filters (search, sort).
$search = isset($this->filters['search']) ? $this->filters['search'] : '';
$sort = isset($this->filters['sort']) ? $this->filters['sort'] : '';
?>
<form class="kb-search" action="<?php echo Route::url('index.php?option=' . $this->option . '&section=all'); ?>" method="get">
<div class="container data-entry">
<input class="entry-search-submit" type="submit" value="<?php echo Lang::txt('COM_KB_SEARCH'); ?>" />
<fieldset class="entry-search">
<legend><?php echo Lang::txt('COM_KB_SEARCH_LEGEND'); ?></legend>
<label for="entry-search-field"><?php echo Lang::txt('COM_KB_SEARCH_LABEL'); ?></label>
<input type="text" name="search" id="entry-search-field" value="<?php echo $this->escape($search); ?>" placeholder="<?php echo Lang::txt('COM_KB_SEARCH_PLACEHOLDER'); ?>" />
<?php if ($sort) { ?>
<input type="hidden" name="sort" value="<?php echo $this->escape($sort); ?>" />
<?php } ?>
</fieldset>
</div><!-- / .container -->
</form>
Loading
Loading