|
<?php |
|
/* |
|
Plugin Name: Show Article Map |
|
Plugin URI: https://www.naenote.net/entry/show-article-map |
|
Description: Visualize internal link between posts |
|
Author: NAE |
|
Version: 0.2 |
|
Author URI: https://www.naenote.net/entry/show-article-map |
|
License: GPL2 |
|
*/ |
|
|
|
namespace NAE\ShowArticleMap; |
|
|
|
use DOMDocument; |
|
use DOMXPath; |
|
use WP_Error; |
|
use WP_Term; |
|
|
|
if ( ! defined( 'ABSPATH' ) ) { |
|
die(); |
|
} |
|
|
|
require_once __DIR__ . '/SingleTon.php'; |
|
|
|
/* |
|
Copyright 2017 NAE (email : @__NAE__) |
|
|
|
This program is free software; you can redistribute it and/or modify |
|
it under the terms of the GNU General Public License, version 2, as |
|
published by the Free Software Foundation. |
|
|
|
This program is distributed in the hope that it will be useful, |
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
GNU General Public License for more details. |
|
|
|
You should have received a copy of the GNU General Public License |
|
along with this program; if not, write to the Free Software |
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
|
*/ |
|
|
|
/** |
|
* Class ShowArticleMap |
|
*/ |
|
class ShowArticleMap extends SingleTon { |
|
|
|
/** |
|
* ShowArticleMap constructor. |
|
*/ |
|
protected function __construct() { |
|
if ( $this->isEnabledShortcode() ) { |
|
add_shortcode( 'article-map', [ $this, 'nae_echo_article_map' ] ); |
|
} |
|
if ( is_admin() ) { |
|
require_once __DIR__ . '/OptionsPage.php'; |
|
Admin\OptionsPage::activate(); |
|
} |
|
} |
|
|
|
/** |
|
* @param string $text |
|
* @param string $insert |
|
* @param int $num |
|
* |
|
* @return string |
|
*/ |
|
public function nae_insert_str( $text, $insert, $num ) { |
|
$return_text = $text; |
|
$text_len = mb_strlen( $text, 'utf-8' ); |
|
$insert_len = mb_strlen( $insert, 'utf-8' ); |
|
for ( $i = 0; ( $i + 1 ) * $num < $text_len; $i ++ ) { |
|
$current_num = $num + $i * ( $insert_len + $num ); |
|
$return_text = preg_replace( "/^.{0,$current_num}+\K/us", $insert, $return_text ); |
|
} |
|
|
|
return $return_text; |
|
} |
|
|
|
/** |
|
* @return false|string |
|
*/ |
|
function nae_get_dataset() { |
|
$args = [ |
|
'posts_per_page' => - 1, |
|
'post_type' => [ 'post', 'page' ], |
|
'post_status' => 'publish', |
|
]; |
|
/** |
|
* @var \WP_Post[] $posts |
|
*/ |
|
$posts = get_posts( $args ); |
|
$nodes = []; |
|
$edges = []; |
|
|
|
$assets = [ |
|
'queue' => [ |
|
'style' => wp_styles()->queue, |
|
'script' => wp_scripts()->queue, |
|
], |
|
'args' => [ |
|
'style' => wp_styles()->args, |
|
'script' => wp_scripts()->args, |
|
], |
|
]; |
|
|
|
foreach ( $posts as $post ) { |
|
if ( is_admin() ) { |
|
$GLOBALS['post'] = $post; |
|
} |
|
/** @var WP_Term[] $categories */ |
|
$categories = get_the_category( $post->ID ); |
|
if ( empty( $categories ) ) { |
|
$group = 'no_cat'; |
|
} else { |
|
$cats = get_ancestors( $categories[0]->cat_ID, 'category' ) ?: $categories; |
|
$root_category_ID = array_pop( $cats ); |
|
|
|
/** @var WP_Term|WP_Error $root_category */ |
|
$root_category = get_category( $root_category_ID ); |
|
$group = empty( $root_category ) || is_wp_error( $root_category ) ? 'no_cat' : $root_category->slug; |
|
} |
|
|
|
$nodes[] = [ |
|
'id' => $post->ID, |
|
'label' => $this->nae_insert_str( urldecode( $post->post_name ), "\r\n", 20 ), |
|
'group' => urldecode( $group ), |
|
'title' => '<a href="' . esc_attr( get_permalink( $post ) ) . '" target="_blank">' . esc_html( $post->post_title ) . '</a>', |
|
]; |
|
|
|
$html = preg_replace( '/\[show_article_map[^\]]*\]/u', '', $post->post_content ); |
|
$html = apply_filters( 'the_content', $html ); |
|
$dom = new DOMDocument; |
|
@$dom->loadHTML( mb_convert_encoding( $html, 'HTML-ENTITIES', 'UTF-8' ) ); |
|
$xpath = new DOMXPath( $dom ); |
|
|
|
$query = "//a[@href != '' and not(starts-with(@href, '#')) and normalize-space() != '']"; |
|
|
|
foreach ( $xpath->query( $query ) as $node ) { |
|
$href = $xpath->evaluate( 'string(@href)', $node ); |
|
$linked_post_id = url_to_postid( $href ); |
|
if ( 0 !== $linked_post_id && ! in_array( [ 'from' => $post->ID, 'to' => $linked_post_id ], $edges, true ) ) { |
|
$edges[] = [ |
|
'from' => $post->ID, |
|
'to' => $linked_post_id, |
|
]; |
|
} |
|
} |
|
wp_reset_postdata(); |
|
} |
|
|
|
// Restore assets. |
|
wp_styles()->queue = $assets['queue']['style']; |
|
wp_styles()->args = $assets['args']['style']; |
|
wp_scripts()->queue = $assets['queue']['script']; |
|
wp_scripts()->args = $assets['args']['script']; |
|
|
|
return wp_json_encode( [ $nodes, $edges ] ); |
|
} |
|
|
|
/** |
|
* @return string |
|
*/ |
|
public function nae_echo_article_map() { |
|
$dataset = $this->nae_get_dataset(); |
|
$body = <<<EOD |
|
<div id="manipulationspace"> |
|
<label for="searchnodequery">Search by node name</label> |
|
<input id="searchnodequery" name="searchnodequery" size="30" style="display:inline;width:50% !important;" type="text"> |
|
<button id="searchnodebutton" type="submit">Search</button> |
|
<button id="deletepagebutton" type="submit">Remove Pages</button> |
|
</div> |
|
<div id="mynetwork" style="width: 100%; height: 800px; border: 1px solid lightgray;"></div> |
|
<div> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.20.0/vis.min.js"></script> |
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.20.0/vis.min.css" rel="stylesheet"> |
|
<script type="text/javascript"> |
|
var dataset = $dataset; |
|
var nodedata = dataset[0]; |
|
var edgedata = dataset[1]; |
|
// create an array with nodes |
|
var nodes = new vis.DataSet(nodedata); |
|
// create an array with edges |
|
var edges = new vis.DataSet(edgedata); |
|
// create a network |
|
var container = document.getElementById('mynetwork'); |
|
// provide the data in the vis format |
|
data = { nodes: nodes, edges: edges }; |
|
var options = { |
|
nodes:{shape:"box"}, |
|
edges:{arrows: {to:{enabled: true, scaleFactor:1, type:'arrow'}}}, |
|
manipulation:{enabled:true}, |
|
}; |
|
|
|
// initialize your network! |
|
var network = new vis.Network(container, data, options); |
|
|
|
// double click node to open an article |
|
network.on('doubleClick', function(e){ |
|
var nodeID = e.nodes.toString(); |
|
var url = jQuery(data.nodes.get(nodeID)['title']).attr('href'); |
|
//console.log(jQuery(data.nodes.get(nodeID)['title'])); console.log(url); |
|
window.open(url,'_blank'); |
|
}); |
|
|
|
// search node label by query |
|
jQuery('#searchnodebutton').click(function(){ |
|
var search = jQuery('#searchnodequery').val(); |
|
|
|
// serch nodes by node label |
|
var hitNodes = nodes.get({ |
|
filter:function(item){ |
|
var label = item.label.replace("\\r\\n",""); |
|
return label.indexOf(search) !== -1; |
|
} |
|
}); |
|
var hitNodeIDs = []; |
|
for (var i=0;i<hitNodes.length;i++) { |
|
hitNodeIDs.push(hitNodes[i].id); |
|
}; |
|
|
|
// select |
|
network.selectNodes(hitNodeIDs); |
|
}); |
|
jQuery('#searchnodequery').keypress(function(e){ |
|
if(e.which === 13){//Enter key pressed |
|
jQuery('#searchnodebutton').click();//Trigger search button click event |
|
} |
|
}); |
|
jQuery('#deletepagebutton').click(function(){ |
|
// serch nodes by group ID |
|
var hitNodes = nodes.get({ |
|
filter:function(item){ |
|
return item.group.indexOf("no_cat") !== -1; |
|
} |
|
}); |
|
nodes.remove(hitNodes); |
|
}); |
|
</script> |
|
</div> |
|
EOD; |
|
|
|
return $body; |
|
} |
|
|
|
protected function isEnabledShortcode() { |
|
$option = get_option( 'nae_article_map_options', [] ); |
|
|
|
return isset( $option['enable_shortcode'] ) && (bool) $option['enable_shortcode']; |
|
} |
|
} |
|
|
|
ShowArticleMap::activate(); |
gistだとディレクトリ構造保てなくね