3,
'max_paragraphs' => 0,
'skip_first_paragraphs' => 0,
'skip_last_paragraphs' => 1,
'min_words_above' => 0,
'min_paragraph_words' => 5,
'paragraph_tags' => 'p',
'count_inside_blockquote' => false,
];
private $settings;
public function __construct(array $overrides = []) {
$this->settings = self::settings($overrides);
}
public static function settings(array $overrides = []) {
$out = [];
foreach (self::DEFAULTS as $key => $default) {
$value = get_option('adghost_' . $key, $default);
if (array_key_exists($key, $overrides) && $overrides[$key] !== '' && $overrides[$key] !== null) {
$value = $overrides[$key];
}
$out[$key] = ($key === 'paragraph_tags')
? (string) $value
: (($key === 'count_inside_blockquote') ? filter_var($value, FILTER_VALIDATE_BOOLEAN) : absint($value));
}
return apply_filters('adghost_paragraph_rules', $out, $overrides);
}
public function all() {
return $this->settings;
}
public static function count_tags(?array $settings = null) {
$raw = $settings['paragraph_tags'] ?? get_option('adghost_paragraph_tags', self::DEFAULTS['paragraph_tags']);
$tags = [];
foreach (preg_split('/[\s,]+/', str_replace(['<', '>'], '', (string) $raw)) as $tag) {
$tag = strtolower(trim($tag));
if ($tag !== '' && preg_match('/^[a-z][a-z0-9]*$/', $tag)) {
$tags[] = $tag;
}
}
return $tags ?: ['p'];
}
public function content_gate($content) {
$total = $this->count_paragraphs($content);
$min = $this->settings['min_paragraphs'];
if ($min > 0 && $total < $min) {
return sprintf(
'Post has %d %s; at least %d required',
$total,
$total === 1 ? 'paragraph' : 'paragraphs',
$min
);
}
$max = $this->settings['max_paragraphs'];
if ($max > 0 && $total > $max) {
return sprintf('Post has %d paragraphs; the maximum is %d', $total, $max);
}
return null;
}
public function count_paragraphs($content) {
return count($this->paragraph_ends($content));
}
public function paragraph_ends($content) {
$tags = self::count_tags($this->settings);
$alt = implode('|', array_map(function ($t) {
return preg_quote($t, '/');
}, $tags));
if (!preg_match_all('/<\/(?:' . $alt . ')>/i', $content, $m, PREG_OFFSET_CAPTURE)) {
return self::block_ends($content);
}
preg_match_all('/<(?:' . $alt . ')\b[^>]*>/i', $content, $opens, PREG_OFFSET_CAPTURE);
$open_at = array_column($opens[0], 1);
$ends = [];
$o = 0;
$last_open = null;
$skip_ranges = $this->settings['count_inside_blockquote'] ? [] : self::element_ranges($content, ['blockquote', 'figure', 'li']);
foreach ($m[0] as $match) {
$end = $match[1] + strlen($match[0]);
foreach ($skip_ranges as [$from, $to]) {
if ($match[1] > $from && $match[1] < $to) {
continue 2;
}
}
while ($o < count($open_at) && $open_at[$o] < $match[1]) {
$last_open = $open_at[$o];
$o++;
}
if ($last_open !== null && !self::has_text(substr($content, $last_open, $match[1] - $last_open))) {
continue;
}
$ends[] = $end;
}
return $ends;
}
private static function has_text($html) {
$text = html_entity_decode(wp_strip_all_tags((string) $html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
return trim(str_replace("\xC2\xA0", ' ', $text)) !== '';
}
public static function block_ends($content) {
if (preg_match_all('/<(\/?)(div|section|article)\b[^>]*>/i', $content, $m, PREG_OFFSET_CAPTURE)) {
$by_depth = [];
$depth = 0;
$opened = [];
foreach ($m[0] as $i => $tag) {
if ($m[1][$i][0] === '') {
$opened[$depth] = $tag[1];
$depth++;
continue;
}
$depth = max(0, $depth - 1);
$start = $opened[$depth] ?? 0;
$end = $tag[1] + strlen($tag[0]);
if (AdGhost_Content_Analyzer::count_words(substr($content, $start, $end - $start)) >= 8) {
$by_depth[$depth][] = $end;
}
}
ksort($by_depth);
foreach ($by_depth as $ends) {
if (count($ends) >= 3) {
return $ends;
}
}
}
if (preg_match_all('/
\s*(?: |\s)*
/i', $content, $m, PREG_OFFSET_CAPTURE)) {
$ends = [];
$prev = 0;
foreach ($m[0] as $match) {
$end = $match[1] + strlen($match[0]);
if (AdGhost_Content_Analyzer::count_words(substr($content, $prev, $match[1] - $prev)) >= 8) {
$ends[] = $end;
}
$prev = $end;
}
if (count($ends) >= 2) {
return $ends;
}
}
return [];
}
public function filter_points(array $points, $content, ?array &$rejected = null) {
$collect = is_array($rejected);
$total = $this->count_paragraphs($content);
$skip_first = $this->settings['skip_first_paragraphs'];
$skip_last = $this->settings['skip_last_paragraphs'];
$min_above = $this->settings['min_words_above'];
$min_words = $this->settings['min_paragraph_words'];
$out = [];
foreach ($points as $p) {
$n = (int) ($p['paragraph_number'] ?? 0);
$reason = null;
if ($skip_first > 0 && $n <= $skip_first) {
$reason = sprintf('Within the first %d %s of the post',
$skip_first, $skip_first === 1 ? 'paragraph' : 'paragraphs');
} elseif ($skip_last > 0 && $n > ($total - $skip_last)) {
$reason = sprintf('Within the last %d %s of the post',
$skip_last, $skip_last === 1 ? 'paragraph' : 'paragraphs');
} elseif ($min_words > 0 && ($p['type'] ?? 'paragraph') === 'paragraph' && isset($p['word_count']) && $p['word_count'] < $min_words) {
$reason = sprintf('Paragraph has %d words; %d required',
(int) $p['word_count'], $min_words);
} elseif ($min_above > 0) {
$above = $this->words_above($content, (int) ($p['offset'] ?? 0));
if ($above < $min_above) {
$reason = sprintf('Only %d words above this point; %d required',
$above, $min_above);
}
}
if ($reason !== null) {
if ($collect) {
$rejected[$p['id']] = $reason;
}
continue;
}
$out[] = $p;
}
return $out;
}
private function words_above($content, $offset) {
return AdGhost_Content_Analyzer::count_words(substr($content, 0, $offset));
}
public static function element_ranges($content, array $tags) {
$ranges = [];
foreach ($tags as $tag) {
if (!preg_match_all('/<(\/?)' . preg_quote($tag, '/') . '\b[^>]*>/i', $content, $m, PREG_OFFSET_CAPTURE)) {
continue;
}
$depth = 0;
$start = 0;
foreach ($m[0] as $i => $t) {
if ($m[1][$i][0] === '') {
if ($depth++ === 0) {
$start = $t[1];
}
} elseif ($depth > 0 && --$depth === 0) {
$ranges[] = [$start, $t[1] + strlen($t[0])];
}
}
if ($depth > 0) {
$ranges[] = [$start, strlen($content)];
}
}
return $ranges;
}
}