summaryrefslogtreecommitdiff
path: root/indexing/zhmakeindex/main.go
blob: 5ba585b4f495926fa30a1dcc31674ed51c95d73b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
// zhmakeindex: 带中文支持的 makeindex 实现
package main // import "github.com/leo-liu/zhmakeindex"

import (
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"runtime/pprof"
	"strings"

	"golang.org/x/text/encoding"
	"golang.org/x/text/encoding/simplifiedchinese"
	"golang.org/x/text/encoding/traditionalchinese"
	"golang.org/x/text/encoding/unicode"
	"golang.org/x/text/transform"
)

var (
	ProgramAuthor = "刘海洋<leoliu.pku@gmail.com>"
	// Build with option: -ldflags "-x main.Version ? -X main.Revision ?"
	Version  = "???"
	Revision = "???"
)

var debug = log.New(os.Stderr, "DEBUG: ", log.Lshortfile)

func init() {
	log.SetFlags(0)
	log.SetPrefix("")
}

func main() {
	option := NewOptions()
	option.parse()

	setupLog(option)

	log.Printf("zhmakeindex 版本:%s-%s\t作者:%s\n", Version, Revision, ProgramAuthor)

	if option.cpuprofile != "" {
		f, err := os.Create(option.cpuprofile)
		if err != nil {
			log.Fatal(err)
		}
		if err := pprof.StartCPUProfile(f); err != nil {
			log.Fatal("无法启动 CPU 性能调试:", err)
		}
		defer pprof.StopCPUProfile()
	}

	if option.style != "" {
		log.Printf("正在读取格式文件 %s……", option.style)
	}
	instyle, outstyle := NewStyles(&option.StyleOptions)

	in := NewInputIndex(&option.InputOptions, instyle)
	log.Printf("合并后共 %d 项。\n", len(*in))

	log.Println("正在排序……")
	out := NewOutputIndex(in, &option.OutputOptions, outstyle)

	log.Println("正在输出……")
	out.Output(&option.OutputOptions)

	if option.output != "" {
		log.Printf("输出文件写入 %s\n", option.output)
	}
	if option.log != "" {
		log.Printf("日志文件写入 %s\n", option.log)
	}
}

type Options struct {
	InputOptions
	OutputOptions
	StyleOptions
	encoding       string
	style_encoding string
	log            string
	quiet          bool
	cpuprofile     string
}

type InputOptions struct {
	compress bool
	stdin    bool
	decoder  transform.Transformer // 由 encoding 生成
	input    []string
}

type OutputOptions struct {
	encoder       transform.Transformer // 由 encoding 生成
	output        string
	sort          string
	page          string
	strict        bool
	disable_range bool
}

type StyleOptions struct {
	style         string
	style_decoder transform.Transformer // 由 style_encoding 生成
}

func NewOptions() *Options {
	o := new(Options)
	flag.BoolVar(&o.compress, "c", false, "忽略条目首尾空格")
	flag.BoolVar(&o.stdin, "i", false, "从标准输入读取")
	flag.StringVar(&o.output, "o", "", "输出文件")
	flag.StringVar(&o.sort, "z", "pinyin",
		"中文分组排序方式,可以使用 pinyin (reading)、bihua (stroke) 或 bushou (radical)")
	// flag.StringVar(&o.page, "p", "", "设置起始页码") // 未实现
	flag.BoolVar(&o.quiet, "q", false, "静默模式,不输出错误信息")
	flag.BoolVar(&o.disable_range, "r", false, "禁用自动生成页码区间")
	flag.BoolVar(&o.strict, "strict", false, "严格区分不同 encapsulated 命令的页码")
	flag.StringVar(&o.style, "s", "", "格式文件名")
	flag.StringVar(&o.log, "t", "", "日志文件名")
	flag.StringVar(&o.encoding, "enc", "utf-8", "读写索引文件的编码")
	flag.StringVar(&o.style_encoding, "senc", "utf-8", "格式文件的编码")
	flag.StringVar(&o.cpuprofile, "cpuprofile", "", "将 CPU 性能调试信息写入文件")
	return o
}

func (o *Options) parse() {
	// 设置自定义帮助信息
	flag.Usage = Usage

	flag.Parse()

	// 整理输入文件
	o.input = flag.Args()
	for i := range o.input {
		o.input[i] = filepath.Clean(o.input[i])
	}

	// 错误的参数组合
	if len(o.input) > 0 && o.stdin {
		log.Fatalln("不能同时从文件和标准输入流读取输入")
	} else if len(o.input) == 0 && !o.stdin {
		// 没有输入文件
		flag.Usage()
		os.Exit(0)
	}
	// 不指定输出文件且不使用标准输入时,使用第一个输入文件的主文件名 + ".ind" 后缀
	if o.output == "" && !o.stdin {
		o.output = stripExt(o.input[0]) + ".ind"
	}
	// 不指定输入文件且不使用标准输入时,使用第一个输入文件的主文件名 + ".ilg" 后缀
	if o.log == "" && !o.stdin {
		o.log = stripExt(o.input[0]) + ".ilg"
	}
	// 不指定格式文件且只有一个输入文件时,尝试使用第一个输入文件的主文件名 + ".mst" 后缀
	if o.style == "" && len(o.input) == 1 {
		// 这里只在当前目录下查找 .mst 文件而不调用 kpathsea 搜索,预先判断文件存在
		mst := stripExt(o.input[0]) + ".mst"
		if _, err := os.Stat(mst); err == nil {
			o.style = mst
		}
	}

	// 检查并设置 IO 编码
	encoding := checkEncoding(o.encoding)
	o.encoder = encoding.NewEncoder()
	o.decoder = encoding.NewDecoder()

	// 检查并设置格式文件编码
	styleEncoding := checkEncoding(o.style_encoding)
	o.style_decoder = styleEncoding.NewDecoder()
}

// 检查编码名,返回编码
func checkEncoding(encodingName string) encoding.Encoding {
	var encodingMap = map[string]encoding.Encoding{
		"utf-8":   encoding.Nop,
		"utf-16":  unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM),
		"gb18030": simplifiedchinese.GB18030,
		"gbk":     simplifiedchinese.GBK,
		"big5":    traditionalchinese.Big5,
	}
	encoding, ok := encodingMap[strings.ToLower(encodingName)]
	if !ok {
		log.Printf("编码 '%s' 是无效编码\n", encodingName)
		var supported string
		for enc := range encodingMap {
			supported += enc + " "
		}
		log.Fatalf("支持的编码有(不区分大小写):%s\n", supported)
	}
	return encoding
}

func setupLog(option *Options) {
	var stderr io.Writer = os.Stderr
	if option.quiet {
		stderr = ioutil.Discard
	}
	if option.log == "" {
		// 只使用标准错误流
		return
	}
	flog, err := os.Create(option.log)
	if err != nil {
		log.Fatalln(err)
	}
	log.SetOutput(io.MultiWriter(stderr, flog))
}

// 删除文件后缀名
func stripExt(fpath string) string {
	ext := filepath.Ext(fpath)
	return fpath[:len(fpath)-len(ext)]
}

// 帮助信息
func Usage() {
	fmt.Fprintln(os.Stderr, `用法:
zhmakeindex [-c] [-i] [-o <ind>] [-q] [-r] [-s <sty>] [-t <log>]
            [-enc <enc>] [-senc <senc>] [-strict] [-z <sort>]
            [<输入文件1> <输入文件2> ...]`)
	fmt.Fprintln(os.Stderr, "\n中文索引处理程序")
	fmt.Fprintf(os.Stderr, "\n  %-10s %-5s %s\n", "选项", "默认值", "说明")
	flag.VisitAll(func(f *flag.Flag) {
		if f.Value.String() == "" {
			fmt.Fprintf(os.Stderr, "  -%-11s %-7s %s\n", f.Name, "无", f.Usage)
		} else {
			fmt.Fprintf(os.Stderr, "  -%-11s %-8s %s\n", f.Name, f.DefValue, f.Usage)
		}
	})
	fmt.Fprintf(os.Stderr, "\n版本:%s-%s\t作者:%s\n", Version, Revision, ProgramAuthor)
}