tdiary-contrib 5.4.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/tdiary/contrib/version.rb +1 -1
- data/plugin/flickr.rb +9 -26
- data/plugin/google_analytics_ga.rb +68 -0
- data/plugin/gyazo.rb +68 -7
- data/plugin/oembed.rb +191 -0
- data/plugin/ogp.rb +13 -3
- data/spec/google_analytics_ga_spec.rb +59 -0
- data/util/tdiary-mode/README.org +69 -0
- data/util/tdiary-mode/tdiary-mode.el +503 -310
- metadata +8 -7
- data/util/tdiary-mode/http.el +0 -130
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12adb1758c3d032d996c055bd197971f0ccd527b3f3c8084b09d9565331b1584
|
|
4
|
+
data.tar.gz: 3e9c54111c414ebd1f2a9867a7c2d9c594557ea54c8aa2e345f9745b1f283743
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 590cea4c3c1440cc140812de2d133e991573e7b216eaaf240be3262152fcacaa38cdfc36c3568339200434f0e37126639f21c582d7b89ec97174b04f1bf58b35
|
|
7
|
+
data.tar.gz: 159563f601640899e155a6ea33e742dc0d293ec7b220f0f749f1d14195c899791ffbd80f0a2a75522cde3d914ce3088d95479f9fe5b1bec1df4cdaf61add3480
|
data/plugin/flickr.rb
CHANGED
|
@@ -59,34 +59,17 @@ def flickr_photo_info(photo_id, size)
|
|
|
59
59
|
|
|
60
60
|
begin
|
|
61
61
|
flickr_open('flickr.photos.getInfo', photo_id) {|f|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
photo[:title] = res.xpath('/rsp/photo/title').text
|
|
66
|
-
else
|
|
67
|
-
res = REXML::Document.new(f)
|
|
68
|
-
photo[:page] = res.elements['//rsp/photo/urls/url'].text
|
|
69
|
-
photo[:title] = res.elements['//rsp/photo/title'].text
|
|
70
|
-
end
|
|
62
|
+
res = REXML::Document.new(f)
|
|
63
|
+
photo[:page] = res.elements['//rsp/photo/urls/url'].text
|
|
64
|
+
photo[:title] = res.elements['//rsp/photo/title'].text
|
|
71
65
|
}
|
|
72
66
|
flickr_open('flickr.photos.getSizes', photo_id) {|f|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
photo[:height] = s.get('height')
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
else
|
|
83
|
-
res = REXML::Document.new(f)
|
|
84
|
-
res.elements.each('//rsp/sizes/size') do |s|
|
|
85
|
-
if s.attributes['label'].downcase == size.downcase
|
|
86
|
-
photo[:src] = s.attributes['source']
|
|
87
|
-
photo[:width] = s.attributes['width']
|
|
88
|
-
photo[:height] = s.attributes['height']
|
|
89
|
-
end
|
|
67
|
+
res = REXML::Document.new(f)
|
|
68
|
+
res.elements.each('//rsp/sizes/size') do |s|
|
|
69
|
+
if s.attributes['label'].downcase == size.downcase
|
|
70
|
+
photo[:src] = s.attributes['source']
|
|
71
|
+
photo[:width] = s.attributes['width']
|
|
72
|
+
photo[:height] = s.attributes['height']
|
|
90
73
|
end
|
|
91
74
|
end
|
|
92
75
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Google Analytics GA4 plugin for tDiary
|
|
3
|
+
#
|
|
4
|
+
# Copyright (C) 2025 KITADAI Yukinori <algebraicallyClosedField@gmail.com>
|
|
5
|
+
# You can redistribute it and/or modify it under GPL2.
|
|
6
|
+
#
|
|
7
|
+
if /^(?:latest|day|month|nyear|search)$/ =~ @mode then
|
|
8
|
+
add_footer_proc do
|
|
9
|
+
google_analytics_ga_insert_code
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def google_analytics_ga_insert_code
|
|
14
|
+
return '' unless @conf['google_analytics_ga.profile']
|
|
15
|
+
<<-HTML
|
|
16
|
+
<!-- Global site tag (gtag.js) - Google Analytics -->
|
|
17
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=#{@conf['google_analytics_ga.profile']}"></script>
|
|
18
|
+
<script>
|
|
19
|
+
window.dataLayer = window.dataLayer || [];
|
|
20
|
+
function gtag(){dataLayer.push(arguments);}
|
|
21
|
+
gtag('js', new Date());
|
|
22
|
+
|
|
23
|
+
gtag('config', '#{@conf['google_analytics_ga.profile']}');
|
|
24
|
+
</script>
|
|
25
|
+
HTML
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# G-XXXXXXXXXX
|
|
29
|
+
add_conf_proc( 'google_analytics_ga', 'Google Analytics GA4' ) do
|
|
30
|
+
if @mode == 'saveconf' then
|
|
31
|
+
@conf['google_analytics_ga.profile'] = @cgi.params['google_analytics_ga.profile'][0]
|
|
32
|
+
end
|
|
33
|
+
r = <<-HTML
|
|
34
|
+
<h3>Google Analytics GA4 Profile</h3>
|
|
35
|
+
<p>set your Profile ID (G-XXXXXXXXXX)</p>
|
|
36
|
+
<p><input name="google_analytics_ga.profile" value="#{h @conf['google_analytics_ga.profile']}"></p>
|
|
37
|
+
HTML
|
|
38
|
+
r
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if defined? AMP
|
|
42
|
+
add_amp_header_proc do
|
|
43
|
+
%Q|<script async custom-element="amp-analytics"
|
|
44
|
+
src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>|
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
add_amp_body_enter_proc do
|
|
48
|
+
profile_id = %w(google_analytics_ga.profile).map {|key|
|
|
49
|
+
@conf[key]
|
|
50
|
+
}.find {|profile|
|
|
51
|
+
profile && !profile.empty?
|
|
52
|
+
}
|
|
53
|
+
<<-HTML
|
|
54
|
+
<amp-analytics type="gtag" data-credentials="include">
|
|
55
|
+
<script type="application/json">
|
|
56
|
+
{
|
|
57
|
+
"vars": {
|
|
58
|
+
"gtag_id": "#{h profile_id}"
|
|
59
|
+
"config" : {
|
|
60
|
+
"#{h profile_id}": { "groups": "default" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
</amp-analytics>
|
|
66
|
+
HTML
|
|
67
|
+
end
|
|
68
|
+
end
|
data/plugin/gyazo.rb
CHANGED
|
@@ -5,19 +5,58 @@
|
|
|
5
5
|
#
|
|
6
6
|
require 'net/http'
|
|
7
7
|
require 'json'
|
|
8
|
+
require 'fileutils'
|
|
8
9
|
|
|
9
10
|
if /^(form|edit|formplugin|showcomment)$/ =~ @mode then
|
|
10
11
|
enable_js('gyazo.js')
|
|
11
12
|
end
|
|
12
13
|
|
|
13
|
-
def
|
|
14
|
-
|
|
14
|
+
def gyazo_extract_id(permalink_url)
|
|
15
|
+
# Extract Gyazo ID from permalink URL
|
|
16
|
+
# e.g., "https://gyazo.com/71a4efdd066f5c2ebd11ee5a37b8f9ef" -> "71a4efdd066f5c2ebd11ee5a37b8f9ef"
|
|
17
|
+
permalink_url.to_s.strip.match(%r{gyazo\.com/([a-f0-9]+)}i)&.captures&.first
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def gyazo_oembed_fetch(permalink_url)
|
|
21
|
+
gyazo_id = gyazo_extract_id(permalink_url)
|
|
22
|
+
return nil unless gyazo_id
|
|
23
|
+
|
|
24
|
+
cache_dir = File.join(@cache_path, 'gyazo')
|
|
25
|
+
cache_file = File.join(cache_dir, "#{gyazo_id}.json")
|
|
26
|
+
|
|
27
|
+
# Load from cache if available (skip cache in preview mode)
|
|
28
|
+
if @mode != 'preview' && File.exist?(cache_file)
|
|
29
|
+
begin
|
|
30
|
+
json_data = File.read(cache_file)
|
|
31
|
+
return JSON.parse(json_data, symbolize_names: true)
|
|
32
|
+
rescue => e
|
|
33
|
+
@logger.warn("gyazo.rb: cache read error for #{gyazo_id}: #{e.message}")
|
|
34
|
+
# Continue to fetch from API if cache read fails
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Fetch from Gyazo OEmbed API
|
|
15
39
|
begin
|
|
16
|
-
|
|
40
|
+
json_data = Net::HTTP.get(URI("https://api.gyazo.com/api/oembed?url=#{permalink_url}"))
|
|
41
|
+
oembed = JSON.parse(json_data, symbolize_names: true)
|
|
42
|
+
|
|
43
|
+
# Save to cache
|
|
44
|
+
FileUtils.mkdir_p(cache_dir) unless File.directory?(cache_dir)
|
|
45
|
+
File.write(cache_file, json_data)
|
|
46
|
+
|
|
47
|
+
oembed
|
|
17
48
|
rescue => e
|
|
18
|
-
@logger.error(e)
|
|
19
|
-
|
|
49
|
+
@logger.error("gyazo.rb: API error for #{permalink_url}: #{e.message}")
|
|
50
|
+
nil
|
|
20
51
|
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def gyazo(permalink_url, alt = '[description]', style = 'photo')
|
|
55
|
+
size = @conf['gyazo_max_size'] || 512
|
|
56
|
+
|
|
57
|
+
oembed = gyazo_oembed_fetch(permalink_url)
|
|
58
|
+
return '' unless oembed
|
|
59
|
+
|
|
21
60
|
url = oembed[:url].gsub(%r|/thumb/\d+/|, "/thumb/#{size}/")
|
|
22
61
|
width = oembed[:width].to_i
|
|
23
62
|
height = oembed[:height].to_i
|
|
@@ -69,11 +108,29 @@ add_form_proc() do |date|
|
|
|
69
108
|
'</div></div>'
|
|
70
109
|
end
|
|
71
110
|
|
|
111
|
+
def gyazo_clear_cache
|
|
112
|
+
cache_dir = File.join(@cache_path, 'gyazo')
|
|
113
|
+
return unless File.directory?(cache_dir)
|
|
114
|
+
|
|
115
|
+
count = 0
|
|
116
|
+
Dir.glob(File.join(cache_dir, '*.json')).each do |cache_file|
|
|
117
|
+
File.delete(cache_file)
|
|
118
|
+
count += 1
|
|
119
|
+
end
|
|
120
|
+
@logger.info("gyazo.rb: cleared #{count} cache files")
|
|
121
|
+
count
|
|
122
|
+
end
|
|
123
|
+
|
|
72
124
|
add_conf_proc('gyazo', 'Gyazo') do
|
|
73
125
|
if @mode == 'saveconf'
|
|
74
126
|
@conf['gyazo_token'] = @cgi.params['gyazo_token'][0]
|
|
75
127
|
@conf['gyazo_max_images'] = @cgi.params['gyazo_max_images'][0].to_i
|
|
76
128
|
@conf['gyazo_max_size'] = @cgi.params['gyazo_max_size'][0].to_i
|
|
129
|
+
|
|
130
|
+
if @cgi.params['gyazo_clear_cache'][0] == 'true'
|
|
131
|
+
count = gyazo_clear_cache
|
|
132
|
+
@conf.save
|
|
133
|
+
end
|
|
77
134
|
end
|
|
78
135
|
@conf['gyazo_max_images'] = 5 if @conf['gyazo_max_images'].to_i < 1
|
|
79
136
|
@conf['gyazo_max_size'] = 512 if @conf['gyazo_max_size'].to_i < 1
|
|
@@ -83,8 +140,12 @@ add_conf_proc('gyazo', 'Gyazo') do
|
|
|
83
140
|
r << %Q|<p>Get your token from <a href="https://gyazo.com/oauth/applications">Gyazo Applications</a></p>|
|
|
84
141
|
r << %Q|<p><input name="gyazo_token" value="#{h @conf['gyazo_token']}" size=64></p>|
|
|
85
142
|
r << %Q|<h3 class="subtitle">Max images in list</h3>|
|
|
86
|
-
r << %Q|<p><input name="
|
|
143
|
+
r << %Q|<p><input name="gyazo_max_images" value="#{h @conf['gyazo_max_images']}" size=3></p>|
|
|
87
144
|
r << %Q|<h3 class="subtitle">Max image size</h3>|
|
|
88
|
-
r << %Q|<p><input name="
|
|
145
|
+
r << %Q|<p><input name="gyazo_max_size" value="#{h @conf['gyazo_max_size']}" size=4> px</p>|
|
|
146
|
+
r << %Q|<h3 class="subtitle">Clear cache</h3>|
|
|
147
|
+
r << %Q|<p><label for="gyazo_clear_cache">|
|
|
148
|
+
r << %Q|<input type="checkbox" id="gyazo_clear_cache" name="gyazo_clear_cache" value="true">|
|
|
149
|
+
r << %Q| Clear all cached OEmbed data</label></p>|
|
|
89
150
|
r
|
|
90
151
|
end
|
data/plugin/oembed.rb
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
#
|
|
3
|
+
# oembed.rb - YouTubeやSpotifyなどが提供する埋め込みタグをoEmbedを利用して取得する
|
|
4
|
+
#
|
|
5
|
+
# 使い方:
|
|
6
|
+
# 日記の中で <%=oembed 'https://www.youtube.com/watch?v=jNQXAC9IVRw' %> などと記述します。
|
|
7
|
+
# <%=oembed 'https://www.youtube.com/watch?v=jNQXAC9IVRw', 500, 200 %> などと書くことで幅や高さを指定することもできます。
|
|
8
|
+
#
|
|
9
|
+
# オプション:
|
|
10
|
+
# url (required): URL
|
|
11
|
+
# width (optional, number): 埋め込みタグの幅(単位: px)
|
|
12
|
+
# height (optional, number): 埋め込みタグの高さ(単位: px)
|
|
13
|
+
#
|
|
14
|
+
# 設定:
|
|
15
|
+
# なし
|
|
16
|
+
#
|
|
17
|
+
# 構築方法:
|
|
18
|
+
# 1. nokolexborをインストールします。以下の方法などからインストールできます。
|
|
19
|
+
# - Gemfile.localに`gem 'nokolexbor'`と記述し、`bundle install`を実行する
|
|
20
|
+
# - `gem install nokolexbor` を実行する
|
|
21
|
+
#
|
|
22
|
+
# その他:
|
|
23
|
+
# - xml形式でoembedのデータを処理する実装はしていません。
|
|
24
|
+
#
|
|
25
|
+
# Copyright (c) 2025 eniehack <http://www.eniehack.net/~eniehack>
|
|
26
|
+
|
|
27
|
+
require "pstore"
|
|
28
|
+
require "nokolexbor"
|
|
29
|
+
require "open-uri"
|
|
30
|
+
require 'json'
|
|
31
|
+
|
|
32
|
+
def fetch_oembed(endpoint_url, target_url, maxwidth = nil, maxheight = nil)
|
|
33
|
+
params = URI.decode_www_form(endpoint_url.query).to_h
|
|
34
|
+
params["url"] = target_url
|
|
35
|
+
params["format"] = "json" if params["format"].nil?
|
|
36
|
+
params["maxheight"] = maxheight unless maxheight.nil?
|
|
37
|
+
params["maxwidth"] = maxwidth unless maxwidth.nil?
|
|
38
|
+
oembed_url = endpoint_url
|
|
39
|
+
oembed_url.query = URI.encode_www_form(params)
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
URI.open(oembed_url,
|
|
43
|
+
'User-Agent' => 'tDiary oEmbed Plugin/1.0',
|
|
44
|
+
read_timeout: 10,
|
|
45
|
+
redirect: false
|
|
46
|
+
) do |response|
|
|
47
|
+
JSON.parse(response.read, symbolize_names: true)
|
|
48
|
+
end
|
|
49
|
+
rescue OpenURI::HTTPError, JSON::ParserError, Timeout::Error => e
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_link_header(link_header, base_url)
|
|
55
|
+
link_pattern = /<([^>]+)>(?:\s*;\s*([^,]+))?/
|
|
56
|
+
|
|
57
|
+
link_header.split(',').each do |link|
|
|
58
|
+
match = link.strip.match(link_pattern)
|
|
59
|
+
next unless match
|
|
60
|
+
|
|
61
|
+
url = match[1]
|
|
62
|
+
params_str = match[2] || ''
|
|
63
|
+
|
|
64
|
+
params = {}
|
|
65
|
+
params_str.scan(/(\w+)=(?:"([^"]+)"|([^;,\s]+))/) do |key, quoted_val, unquoted_val|
|
|
66
|
+
params[key] = quoted_val || unquoted_val
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if params['rel'] == 'alternate' &&
|
|
70
|
+
params['type'] == 'application/json+oembed' then
|
|
71
|
+
return URI.join(base_url, url).to_s
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# HTMLからoEmbedエンドポイントを探す
|
|
79
|
+
def discover_oembed_endpoint(url)
|
|
80
|
+
begin
|
|
81
|
+
URI.open(url,
|
|
82
|
+
'User-Agent' => 'tDiary oEmbed Plugin/1.0',
|
|
83
|
+
read_timeout: 5
|
|
84
|
+
) do |response|
|
|
85
|
+
if response.meta["link"] then
|
|
86
|
+
link = parse_link_header(response.meta["link"], url)
|
|
87
|
+
return link if link
|
|
88
|
+
end
|
|
89
|
+
doc = Nokolexbor::HTML(response.read)
|
|
90
|
+
link = doc.at_xpath('//link[@rel="alternate" and @type="application/json+oembed"]/@href')
|
|
91
|
+
return link.to_s if link
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
rescue => e
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def fallback_html(url)
|
|
100
|
+
%Q|<a href="#{url}">#{url}</a>|
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def generate_general_oembed_endpoint(url)
|
|
104
|
+
parsed_url = URI.parse(url)
|
|
105
|
+
general_params = URI.decode_www_form(parsed_url.query).filter { |arr| arr[0] != 'url' }.to_h
|
|
106
|
+
general_params['format'] = 'json' unless general_params['format']
|
|
107
|
+
parsed_url.query = URI.encode_www_form(general_params)
|
|
108
|
+
parsed_url
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def get_oembed_endpoint(url, db)
|
|
112
|
+
uri = URI.parse(url)
|
|
113
|
+
domain = uri.host.downcase
|
|
114
|
+
|
|
115
|
+
db.transaction do |store|
|
|
116
|
+
store[:endpoints] ||= {}
|
|
117
|
+
cached_endpoint = store[:endpoints][domain]
|
|
118
|
+
|
|
119
|
+
if cached_endpoint && Time.now < cached_endpoint[:expires_at]
|
|
120
|
+
return URI.parse(cached_endpoint[:url])
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# cacheやHTMLなどからoEmbedエンドポイントを探す
|
|
125
|
+
discovered_endpoint = discover_oembed_endpoint(url)
|
|
126
|
+
if discovered_endpoint
|
|
127
|
+
# 相対URLを絶対URLに変換
|
|
128
|
+
discovered_endpoint = URI.join(url, discovered_endpoint).to_s
|
|
129
|
+
|
|
130
|
+
# ディスカバリー結果をキャッシュに保存(短期間)
|
|
131
|
+
endpoint = generate_general_oembed_endpoint(discovered_endpoint)
|
|
132
|
+
db.transaction do |store|
|
|
133
|
+
store[:endpoints][domain] = {
|
|
134
|
+
url: endpoint,
|
|
135
|
+
discovered_at: Time.now,
|
|
136
|
+
expires_at: Time.now + (3600 * 24 * 7)
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
return endpoint
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def build_html(json, url)
|
|
146
|
+
case json[:type]
|
|
147
|
+
when "rich"
|
|
148
|
+
%Q|<div style="width: #{json[:width]}px;">#{json[:html]}</div>|
|
|
149
|
+
when "video"
|
|
150
|
+
%Q|<div style="width: #{json[:width]}px;">#{json[:html]}</div>|
|
|
151
|
+
when "image"
|
|
152
|
+
%Q|<div style="width: #{json[:width]}px;"><img src="#{json[:url]}" height="#{json[:height]}" width="#{json[:width]}" /></div>|
|
|
153
|
+
when "link"
|
|
154
|
+
%Q|<div><a href="#{url}">#{json[:title]}</a></div>|
|
|
155
|
+
else
|
|
156
|
+
fallback_html(url)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def oembed(url, width = nil, height = nil)
|
|
161
|
+
cache_file = "#{@cache_path}/oembed/cache.pstore"
|
|
162
|
+
|
|
163
|
+
begin
|
|
164
|
+
db = PStore.new(cache_file)
|
|
165
|
+
db.transaction do |store|
|
|
166
|
+
cached = store[:responses][url] unless store[:responses].nil?
|
|
167
|
+
if cached && cached[:expires_at] && Time.now < cached[:expires_at]
|
|
168
|
+
return build_html(cached[:json], url)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# サービスのoEmbedエンドポイントをcacheまたはHTMLから探す
|
|
173
|
+
endpoint = get_oembed_endpoint(url, db)
|
|
174
|
+
return fallback_html(url) unless endpoint
|
|
175
|
+
|
|
176
|
+
# oEmbedによる埋め込みコードの取得
|
|
177
|
+
oembed_data = fetch_oembed(endpoint, url, width, height)
|
|
178
|
+
return fallback_html(url) unless oembed_data
|
|
179
|
+
|
|
180
|
+
db.transaction do |store|
|
|
181
|
+
store[:responses] ||= {}
|
|
182
|
+
store[:responses][url] = {
|
|
183
|
+
json: oembed_data,
|
|
184
|
+
expires_at: Time.now + (3600 * 24 * 3)
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
build_html(oembed_data, url)
|
|
188
|
+
rescue => e
|
|
189
|
+
fallback_html(url)
|
|
190
|
+
end
|
|
191
|
+
end
|
data/plugin/ogp.rb
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
# @conf['ogp.facebook.app_id'] - your facebook application ID.
|
|
8
8
|
# @conf['ogp.facebook.admins'] - your facebook ID.
|
|
9
|
+
# @conf['ogp.fediverse.creator'] - your fediverse ID.
|
|
9
10
|
|
|
10
11
|
def ogp_description(html)
|
|
11
12
|
@conf.shorten(remove_tag(html), 200)
|
|
@@ -28,7 +29,8 @@ def ogp_tag
|
|
|
28
29
|
ogp = ogp_tag_org || ''
|
|
29
30
|
headers = {
|
|
30
31
|
'fb:app_id' => @conf['ogp.facebook.app_id'],
|
|
31
|
-
'fb:admins' => @conf['ogp.facebook.admins']
|
|
32
|
+
'fb:admins' => @conf['ogp.facebook.admins'],
|
|
33
|
+
'fediverse:creator' => @conf['ogp.fediverse.creator']
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
if @mode == 'day'
|
|
@@ -60,8 +62,13 @@ end
|
|
|
60
62
|
|
|
61
63
|
add_conf_proc('Open Graph Protocol', 'Open Graph Protocol') do
|
|
62
64
|
if @mode == 'saveconf'
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
%w(
|
|
66
|
+
ogp.facebook.app_id
|
|
67
|
+
ogp.facebook.admins
|
|
68
|
+
ogp.fediverse.creator
|
|
69
|
+
).each do |name|
|
|
70
|
+
@conf[name] = @cgi.params[name][0]
|
|
71
|
+
end
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
<<-HTML
|
|
@@ -70,5 +77,8 @@ add_conf_proc('Open Graph Protocol', 'Open Graph Protocol') do
|
|
|
70
77
|
|
|
71
78
|
<h3>Facebook User ID</h3>
|
|
72
79
|
<p><input name="ogp.facebook.admins" value="#{h(@conf['ogp.facebook.admins'])}"></p>
|
|
80
|
+
|
|
81
|
+
<h3>Fediverse ID</h3>
|
|
82
|
+
<p><input name="ogp.fediverse.creator" value="#{h(@conf['ogp.fediverse.creator'])}"></p>
|
|
73
83
|
HTML
|
|
74
84
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
|
2
|
+
require 'spec_helper'
|
|
3
|
+
|
|
4
|
+
describe "google_analytics_ga plugin" do
|
|
5
|
+
def setup_google_analytics_ga_plugin(profile_id, mode)
|
|
6
|
+
fake_plugin(:google_analytics_ga) { |plugin|
|
|
7
|
+
plugin.mode = mode
|
|
8
|
+
plugin.conf['google_analytics_ga.profile'] = profile_id
|
|
9
|
+
}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe "should render javascript" do
|
|
13
|
+
before do
|
|
14
|
+
@plugin = setup_google_analytics_ga_plugin('G-XXXXXXXXXX', 'latest')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "for footer" do
|
|
18
|
+
snippet = @plugin.footer_proc
|
|
19
|
+
expect(snippet).to eq(expected_html_footer_snippet)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe "should render javascript" do
|
|
24
|
+
before do
|
|
25
|
+
@plugin = setup_google_analytics_ga_plugin('G-XXXXXXXXXX', 'conf')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "for footer" do
|
|
29
|
+
snippet = @plugin.footer_proc
|
|
30
|
+
expect(snippet).to be_empty
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "should not render when profile_id is empty" do
|
|
35
|
+
before do
|
|
36
|
+
@plugin = setup_google_analytics_ga_plugin(nil, 'latest')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "for footer" do
|
|
40
|
+
snippet = @plugin.footer_proc
|
|
41
|
+
expect(snippet).to be_empty
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def expected_html_footer_snippet
|
|
46
|
+
expected = <<-SCRIPT
|
|
47
|
+
<!-- Global site tag (gtag.js) - Google Analytics -->
|
|
48
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
|
|
49
|
+
<script>
|
|
50
|
+
window.dataLayer = window.dataLayer || [];
|
|
51
|
+
function gtag(){dataLayer.push(arguments);}
|
|
52
|
+
gtag('js', new Date());
|
|
53
|
+
|
|
54
|
+
gtag('config', 'G-XXXXXXXXXX');
|
|
55
|
+
</script>
|
|
56
|
+
SCRIPT
|
|
57
|
+
expected.gsub( /^\t/, '' ).chomp
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
* What's this?
|
|
2
|
+
Emacs から tDiary を更新するための Major mode です.
|
|
3
|
+
* How to Use
|
|
4
|
+
load-path の通った所に [[file:tdiary-mode.el][tdiary-mode.el]] を置いて,
|
|
5
|
+
以下を =.emacs= に記載して下さい.
|
|
6
|
+
#+BEGIN_SRC emacs-lisp
|
|
7
|
+
;; 更新する日記のリスト
|
|
8
|
+
(setq tdiary-diary-list '(("my 1st diary" "http://example.com/tdiary/")
|
|
9
|
+
("my 2nd diary" "http://example.com/tdiary2/")))
|
|
10
|
+
;; 更新後に日記原稿を保存するディレクトリ
|
|
11
|
+
(setq tdiary-text-directory (expand-file-name "~/path-to-saved-diary"))
|
|
12
|
+
;; preview に使う関数
|
|
13
|
+
(setq tdiary-browser-function 'browse-url)
|
|
14
|
+
;; CSRF key ;; tdiary.conf から csrf.protection.key をコピーする
|
|
15
|
+
(setq tdiary-csrf-key "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
|
|
16
|
+
;; スタイルに応じて: org スタイル
|
|
17
|
+
(setq tdiary-style-mode 'org-mode
|
|
18
|
+
tdiary-text-suffix ".org")
|
|
19
|
+
;; スタイルに応じて: tDiary as html スタイル
|
|
20
|
+
;; (setq tdiary-style-mode 'html-mode
|
|
21
|
+
;; tdiary-text-suffix ".td")
|
|
22
|
+
;; スタイルに応じて: gfm スタイル
|
|
23
|
+
;; (setq tdiary-style-mode 'markdown-mode
|
|
24
|
+
;; tdiary-text-suffix ".md")
|
|
25
|
+
;; スタイルに応じて: RD スタイル
|
|
26
|
+
;; (setq tdiary-style-mode 'rd-mode
|
|
27
|
+
;; tdiary-text-suffix ".rd")
|
|
28
|
+
;; 以下, お好みで: テンプレートとして登録する plugin の定義
|
|
29
|
+
;; (setq tdiary-plugin-definition
|
|
30
|
+
;; '(
|
|
31
|
+
;; ("STRONG" ("<%=STRONG %Q|" (p "str: ") "| %>"))
|
|
32
|
+
;; ("PRE" ("<%=PRE %Q|" (p "str: ") "| %>"))
|
|
33
|
+
;; ("CITE" ("<%=CITE %Q|" (p "str: ") "| %>"))
|
|
34
|
+
;; ))
|
|
35
|
+
;; (add-hook 'tdiary-mode-hook
|
|
36
|
+
;; '(lambda ()
|
|
37
|
+
;; (local-set-key "\C-i" 'tdiary-complete-plugin)))
|
|
38
|
+
#+END_SRC
|
|
39
|
+
|
|
40
|
+
パスワードを保存したい場合には
|
|
41
|
+
#+BEGIN_SRC emacs-lisp
|
|
42
|
+
(setq tdiary-passwd-file (locate-user-emacs-file "tdiary-passwd" ".tdiary-passwd"))
|
|
43
|
+
#+END_SRC
|
|
44
|
+
とでもしておいて
|
|
45
|
+
#+BEGIN_EXAMPLE
|
|
46
|
+
M-x tdiary-password-save
|
|
47
|
+
#+END_EXAMPLE
|
|
48
|
+
として下さい(そのうち =auth-source= に対応する予定です).
|
|
49
|
+
* Notice
|
|
50
|
+
上手く書き込みできない場合にはご自身のtDiaryで
|
|
51
|
+
CSRFの設定がどうなっているかを確認してください.
|
|
52
|
+
tDiaryが動いているマシンにsshできるのであればCSRF KEYは
|
|
53
|
+
=tdiary.conf= に書いてあります.
|
|
54
|
+
|
|
55
|
+
sshができない場合は何かしらの方法でCSRF KEYを知る必要がありますが,
|
|
56
|
+
良いアイデアがありません.
|
|
57
|
+
* ToDO [1/4]
|
|
58
|
+
|
|
59
|
+
- [ ] 認証情報を =auth-source= で扱う
|
|
60
|
+
- [X] Customize 対応
|
|
61
|
+
- [ ] テストの作成
|
|
62
|
+
- [ ] melpa への登録申請?
|
|
63
|
+
* LICENSE
|
|
64
|
+
#+BEGIN_EXAMPLE
|
|
65
|
+
Copyright (C) 2002 Junichiro Kita
|
|
66
|
+
2017-2026 Youhei SASAKI
|
|
67
|
+
License: GPL-2.0+
|
|
68
|
+
#+END_EXAMPLE
|
|
69
|
+
@see [[file:LICENSE][LICENSE]] for detail.
|