Table of Contents 플러그인 개발해보았다. chatGPT와 몇 시간 대화하면서 다듬었다. 예전에는 간단한 플러그인 하나 개발하려면 정말 머리아픈 과정을 거쳐야했는데, AI 세상 너무 편해졌다. 우리 개발자들 이제 뭐 먹고 사나 싶다.
Table of Contents
Table of Contents 플러그인 설명서
MyPlugin TOC Toggle
MyPlugin TOC Toggle은 워드프레스 포스트에 목차(Table of Contents, TOC)를 추가하고, 위젯을 통해 포스트에 대한 목차를 표시할 수 있는 플러그인입니다. 이 플러그인은 사용자가 포스트의 본문과 위젯에서 목차를 활성화할 수 있도록 설정할 수 있습니다.
주요 기능
목차 표시:
- 포스트 본문에 목차를 추가할 수 있습니다.
- 포스트에서 목차를 표시할지 여부를 개별적으로 설정할 수 있습니다.
위젯 지원:
- 위젯을 통해 목차를 표시할 수 있습니다.
- 위젯에서 어떤 포스트 타입에 대해 목차를 표시할지 선택할 수 있습니다.
다양한 설정 옵션:
- 목차 제목의 최대 길이를 설정할 수 있습니다.
- 목차에 표시할 제목의 최대 레벨(깊이)을 설정할 수 있습니다.
- 목차를 기본적으로 접힌 상태로 표시할지 여부를 설정할 수 있습니다.
설치 및 설정 방법
플러그인 설치:
- 워드프레스 관리자 페이지에서
플러그인
>새로 추가
를 클릭합니다. MyPlugin TOC Toggle
을 검색하여 설치하고 활성화합니다.
위젯 설정:
외모
>위젯
메뉴로 이동합니다.MyPlugin Table of Contents
위젯을 추가하고, 원하는 위치에 배치합니다.- 위젯 설정에서 다음 옵션을 설정합니다:
- Max Title Length: 목차 제목의 최대 길이.
- Max Level: 목차에 표시할 제목의 최대 레벨.
- Collapsed by Default: 목차를 기본적으로 접힌 상태로 표시할지 여부.
- Post Types: 목차를 표시할 포스트 타입을 선택합니다 (예: 포스트, 페이지 등).
포스트별 설정:
- 포스트 작성 또는 편집 화면으로 이동합니다.
- 우측 사이드바에
Table of Contents
메타박스가 추가됩니다. - 다음 옵션을 설정합니다:
- Show Table of Contents: 이 포스트에 목차를 표시할지 여부를 선택합니다.
- Show in Widget Also: 위젯에도 이 포스트의 목차를 표시할지 여부를 선택합니다.
- Max Level: 목차에 표시할 제목의 최대 레벨.
- Collapsed by Default: 목차를 기본적으로 접힌 상태로 표시할지 여부.
포스트 저장:
- 설정을 완료한 후, 포스트를 저장하거나 업데이트합니다.
- 설정에 따라 포스트 본문과 위젯에 목차가 표시됩니다.
사용 예시
- 포스트 작성 시 우측 사이드바의
Table of Contents
메타박스에서Show Table of Contents
를 체크하면 본문에 목차가 추가됩니다. Show in Widget Also
를 체크하면, 해당 포스트가 위젯에서도 목차로 표시됩니다.- 위젯 설정에서 선택한 포스트 타입의 모든 포스트에 대해 목차를 표시할 수 있습니다.
이 플러그인을 사용하면 사용자가 길고 복잡한 포스트에서도 쉽게 내용을 탐색할 수 있도록 도와줍니다. 다양한 설정 옵션을 통해 목차의 표시 방식을 자유롭게 조정할 수 있습니다.
TOC 플러그인 코딩
TOC 플러그인을 구지 직접 코딩하는 이유는 자신이 원하는 스타일로 원하는 위치에 삽입하기 위해서다. TOC 스타일링은 생각보다 쉽지 않다. 아래 코딩을 참고해서 자신에게 적합하게 수정하면 좋은 TOC 디자인을 만들 수 있을 것이다.
Table of Contents 플러그인 – Markdown
아래와 같은 구조로 myplugin-toc-toggle폴더를 만들고 그것을 워드프레스 plugins 폴더에 복붙하면 플러그인을 사용할 수 있다.
myplugin-toc-toggle/
│
├── myplugin-toc-toggle.php
│
├── js/
│ └── toc-toggle.js
│
└── css/
└── toc-style.css
myplugin-toc-toggle.php
<?php
/*
Plugin Name: MyPlugin TOC Toggle
Description: Adds a table of contents with toggle functionality to posts.
Version: 1.0
Author: Your Name
*/
// Register and enqueue CSS and JS files
function myplugin_enqueue_scripts() {
wp_enqueue_style('myplugin-toc-style', plugins_url('css/toc-style.css', __FILE__));
wp_enqueue_script('myplugin-toc-toggle', plugins_url('js/toc-toggle.js', __FILE__), array('jquery'), null, true);
}
add_action('wp_enqueue_scripts', 'myplugin_enqueue_scripts');
class MyPlugin_Table_Of_Contents_Widget extends WP_Widget
{
public function __construct()
{
parent::__construct(
'myplugin_table_of_contents_widget', // Base ID
'Table of Contents', // Name
array('description' => __('Displays a table of contents', 'text_domain'), ) // Args
);
}
public function widget($args, $instance)
{
global $post;
// Get the selected post types from the widget instance
$post_types = !empty($instance['post_types']) ? $instance['post_types'] : array('post');
if (!in_array($post->post_type, $post_types)) {
return;
}
// Check if TOC should be shown in widget
$show_in_widget = get_post_meta($post->ID, '_myplugin_show_in_widget', true);
if ($show_in_widget !== 'yes') {
return;
}
// Existing code...
$show_toc = get_post_meta($post->ID, '_myplugin_show_toc', true);
if ($show_toc !== 'yes') {
return;
}
$max_length = !empty($instance['max_length']) ? $instance['max_length'] : '';
$max_level = !empty($instance['max_level']) ? $instance['max_level'] : '6';
$collapsed = !empty($instance['collapsed']) ? $instance['collapsed'] : '';
$content = $post->post_content;
$pattern = '/<h([1-6])[^>]*>(.*?)<\/h\1>/i';
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
if (empty($matches)) {
return;
}
echo $args['before_widget'];
echo '<div class="widget-title widget-title-mywidget-table-of-contents toc-title"><h3>' . apply_filters('widget_title', 'Table of Contents') . '</h3></div>';
echo '<div class="mywidget-table-of-contents' . ($collapsed == 'on' ? ' collapsed' : '') . '"><ul>';
foreach ($matches as $match) {
$level = $match[1];
if ($level > $max_level) {
continue;
}
$title = strip_tags($match[2]);
$id = sanitize_title($title) . '-' . $level;
if ($max_length && strlen($title) > $max_length) {
$title = mb_substr($title, 0, $max_length) . '...';
}
$new_heading = '<h' . $level . ' id="' . $id . '">' . $match[2] . '</h' . $level . '>';
$content = str_replace($match[0], $new_heading, $content);
echo '<li class="mywidget-toc-level-' . $level . '"><a href="#' . $id . '">' . $title . '</a></li>';
}
echo '</ul></div>';
echo $args['after_widget'];
$post->post_content = $content;
}
public function form($instance)
{
$max_length = !empty($instance['max_length']) ? $instance['max_length'] : '';
$max_level = !empty($instance['max_level']) ? $instance['max_level'] : '6';
$collapsed = !empty($instance['collapsed']) ? $instance['collapsed'] : '';
$post_types = !empty($instance['post_types']) ? $instance['post_types'] : array('post');
$available_post_types = get_post_types(array('public' => true), 'objects');
?>
<p>
<label for="<?php echo $this->get_field_id('max_length'); ?>"><?php _e('Max Title Length:'); ?></label>
<input class="widefat" id="<?php echo $this->get_field_id('max_length'); ?>"
name="<?php echo $this->get_field_name('max_length'); ?>" type="number"
value="<?php echo esc_attr($max_length); ?>" />
</p>
<p>
<label for="<?php echo $this->get_field_id('max_level'); ?>"><?php _e('Max Level:'); ?></label>
<input class="widefat" id="<?php echo $this->get_field_id('max_level'); ?>"
name="<?php echo $this->get_field_name('max_level'); ?>" type="number" min="1" max="6"
value="<?php echo esc_attr($max_level); ?>" />
</p>
<p>
<label for="<?php echo $this->get_field_id('collapsed'); ?>"><?php _e('Collapsed by Default:'); ?></label>
<input class="checkbox" type="checkbox" <?php checked($instance['collapsed'], 'on'); ?>
id="<?php echo $this->get_field_id('collapsed'); ?>" name="<?php echo $this->get_field_name('collapsed'); ?>" />
</p>
<p>
<label for="<?php echo $this->get_field_id('post_types'); ?>"><?php _e('Post Types:'); ?></label>
<select class="widefat" id="<?php echo $this->get_field_id('post_types'); ?>"
name="<?php echo $this->get_field_name('post_types'); ?>[]" multiple="multiple">
<?php foreach ($available_post_types as $post_type) : ?>
<option value="<?php echo esc_attr($post_type->name); ?>" <?php echo in_array($post_type->name, $post_types) ? 'selected="selected"' : ''; ?>><?php echo esc_html($post_type->label); ?></option>
<?php endforeach; ?>
</select>
</p>
<?php
}
public function update($new_instance, $old_instance)
{
$instance = array();
$instance['max_length'] = (!empty($new_instance['max_length'])) ? sanitize_text_field($new_instance['max_length']) : '';
$instance['max_level'] = (!empty($new_instance['max_level'])) ? sanitize_text_field($new_instance['max_level']) : '6';
$instance['collapsed'] = (!empty($new_instance['collapsed'])) ? sanitize_text_field($new_instance['collapsed']) : '';
$instance['post_types'] = (!empty($new_instance['post_types'])) ? array_map('sanitize_text_field', $new_instance['post_types']) : array('post');
return $instance;
}
}
function myplugin_register_widgets()
{
register_widget('MyPlugin_Table_Of_Contents_Widget');
}
add_action('widgets_init', 'myplugin_register_widgets');
function myplugin_add_meta_box()
{
// Get the widget instances to retrieve the selected post types
$widget_instances = get_option('widget_myplugin_table_of_contents_widget');
$post_types = array('post'); // Default post type
if ($widget_instances) {
foreach ($widget_instances as $instance) {
if (isset($instance['post_types']) && is_array($instance['post_types'])) {
$post_types = array_merge($post_types, $instance['post_types']);
}
}
$post_types = array_unique($post_types);
}
// Add meta box for each selected post type
foreach ($post_types as $post_type) {
add_meta_box(
'myplugin_toc_meta_box',
'Table of Contents',
'myplugin_render_meta_box',
$post_type,
'side',
'high'
);
}
}
add_action('add_meta_boxes', 'myplugin_add_meta_box');
function myplugin_render_meta_box($post)
{
// 이미 저장된 값 가져오기
$show_toc = get_post_meta($post->ID, '_myplugin_show_toc', true);
$max_level = get_post_meta($post->ID, '_myplugin_max_level', true);
$collapsed = get_post_meta($post->ID, '_myplugin_collapsed', true);
$show_in_widget = get_post_meta($post->ID, '_myplugin_show_in_widget', true);
// 기본값 설정
$show_toc = ($show_toc === '') ? 'yes' : $show_toc;
$max_level = ($max_level === '') ? '6' : $max_level;
$collapsed = ($collapsed === '') ? '' : $collapsed;
$show_in_widget = ($show_in_widget === '') ? '' : $show_in_widget;
// 메타박스 출력
wp_nonce_field('myplugin_save_meta_box_data', 'myplugin_meta_box_nonce');
?>
<p>
<label for="myplugin_show_toc">
<input type="checkbox" id="myplugin_show_toc" name="myplugin_show_toc" value="yes" <?php checked($show_toc, 'yes'); ?> />
Show Table of Contents
</label>
</p>
<p id="show_in_widget_option" style="display: <?php echo ($show_toc == 'yes') ? 'block' : 'none'; ?>;">
<label for="myplugin_show_in_widget">
<input type="checkbox" id="myplugin_show_in_widget" name="myplugin_show_in_widget" value="yes" <?php checked($show_in_widget, 'yes'); ?> />
Show in Widget Also
</label>
</p>
<p>
<label for="myplugin_max_level">Max Level:</label>
<input type="number" id="myplugin_max_level" name="myplugin_max_level" min="1" max="6"
value="<?php echo esc_attr($max_level); ?>" />
</p>
<p>
<label for="myplugin_collapsed">
<input type="checkbox" id="myplugin_collapsed" name="myplugin_collapsed" value="yes" <?php checked($collapsed, 'yes'); ?> />
Collapsed by Default
</label>
</p>
<script>
document.getElementById('myplugin_show_toc').addEventListener('change', function() {
document.getElementById('show_in_widget_option').style.display = this.checked ? 'block' : 'none';
});
</script>
<?php
}
function myplugin_save_meta_box_data($post_id)
{
// 저장하기 전에
if (!isset($_POST['myplugin_meta_box_nonce']) || !wp_verify_nonce($_POST['myplugin_meta_box_nonce'], 'myplugin_save_meta_box_data')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
// 메타 데이터 저장
if (isset($_POST['myplugin_show_toc'])) {
update_post_meta($post_id, '_myplugin_show_toc', 'yes');
} else {
update_post_meta($post_id, '_myplugin_show_toc', 'no');
}
if (isset($_POST['myplugin_max_level'])) {
update_post_meta($post_id, '_myplugin_max_level', sanitize_text_field($_POST['myplugin_max_level']));
}
if (isset($_POST['myplugin_collapsed'])) {
update_post_meta($post_id, '_myplugin_collapsed', 'yes');
} else {
update_post_meta($post_id, '_myplugin_collapsed', 'no');
}
if (isset($_POST['myplugin_show_in_widget'])) {
update_post_meta($post_id, '_myplugin_show_in_widget', 'yes');
} else {
update_post_meta($post_id, '_myplugin_show_in_widget', 'no');
}
}
add_action('save_post', 'myplugin_save_meta_box_data');
function myplugin_add_table_of_contents($content)
{
global $post;
$show_toc = get_post_meta($post->ID, '_myplugin_show_toc', true);
$max_level = get_post_meta($post->ID, '_myplugin_max_level', true);
$collapsed = get_post_meta($post->ID, '_myplugin_collapsed', true);
if ($show_toc !== 'yes') {
return $content;
}
$pattern = '/<h([1-6])[^>]*>(.*?)<\/h\1>/i';
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
if (count($matches) < 3) {
return $content;
}
$toc_title_style = 'font-weight: bold; font-size: 1.1em; cursor: pointer;';
$output = '<div class="myplugin-table-of-contents' . ($collapsed == 'yes' ? ' collapsed' : '') . '">';
$output .= '<p class="toc-title" style="' . esc_attr($toc_title_style) . '">Table of Contents</p>';
$output .= '<ul>';
foreach ($matches as $match) {
$level = $match[1];
if ($level > $max_level) {
continue;
}
$title = strip_tags($match[2]);
$id = sanitize_title($title) . '-' . $level;
$new_heading = '<h' . $level . ' id="' . $id . '">' . $match[2] . '</h' . $level . '>';
$content = str_replace($match[0], $new_heading, $content);
$output .= '<li class="myplugin-toc-level-' . $level . '"><a href="#' . $id . '">' . $title . '</a></li>';
}
$output .= '</ul></div>';
$content = $output . $content;
return $content;
}
add_filter('the_content', 'myplugin_add_table_of_contents');
toc-toggle.js
jQuery(document).ready(function($) {
$('.toc-title').click(function() {
$(this).siblings('ul').slideToggle();
$(this).parent().toggleClass('collapsed');
});
});
toc-style.css
.myplugin-table-of-contents ul {
list-style-type: none;
padding-left: 0;
}
.myplugin-table-of-contents.collapsed ul {
display: none;
}
.toc-title {
cursor: pointer;
}