Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.1.023.4 KB2026-09-14
Version
0.1.0
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-09-14
Size
23.4 KB
Downloads
1
Checksum
298d198fbc3fc0aecbe6668651a0d3beefb7ff2d1a098483af80cc924d925bdd
Dependencies
None

format.rs

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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
//! 値の木を文字列に書き出す。
//!
//! 原型の jsonfmt の format.rs をもとにしている。
//! 原型と違い、max_width の計算にはキーの長さも入れ、行全体が幅に収まるかを見る。

use crate::value::Value;
use crate::{Indent, Options};
use std::borrow::Cow;


/// max_width の計算で、タブ 1 個を何桁と数えるか
const TAB_WIDTH: usize = 4;

/// 幅を見ないときの room
const UNLIMITED: usize = usize::MAX;

/// インデントを書くときに、まとめて写す空白とタブ
const SPACES: &str = "                                ";
const TABS: &str = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";

const HEX: &[u8; 16] = b"0123456789abcdef";


/// 値を書き出す。末尾に改行は付けない。capacity は最初に確保しておく大きさ
pub fn write(mut value: Value, options: &Options, capacity: usize) -> String {
	if options.sort_keys {
		sort_keys(&mut value);
	}

	let mut out = String::with_capacity(capacity);
	if options.indent == Indent::Compact {
		// 幅を見ないので、失敗することはない
		let mut room = UNLIMITED;
		let _ = write_inline(&mut out, &value, options, false, &mut room);
	} else {
		write_pretty(&mut out, &value, options, 0, 0, 0);
	}
	out
}


/// キーを辞書順に並べ替える。入れ子の中まで効く。並べ替えは安定なので、同じキーは出てきた順のまま
fn sort_keys(value: &mut Value) {
	match value {
		Value::Array(items) => {
			for item in items {
				sort_keys(item);
			}
		}
		Value::Object(members) => {
			members.sort_by(|a, b| a.0.cmp(&b.0));
			for (_, item) in members {
				sort_keys(item);
			}
		}
		_ => {}
	}
}


/// 配列とオブジェクトを 1 要素 1 行に展開して書く。
///
/// used はこの値より前にその行で使っている桁数、trailing はこの値の後ろに同じ行で続く桁数。
/// どちらも max_width に収まるかを見るときにだけ使う。
fn write_pretty(out: &mut String, value: &Value, options: &Options, depth: usize, used: usize, trailing: usize) {
	match value {
		Value::Array(items) if !items.is_empty() => {
			if try_inline(out, value, options, used, trailing) {
				return;
			}

			out.push('[');
			for (index, item) in items.iter().enumerate() {
				if index > 0 {
					out.push(',');
				}
				new_line(out, options, depth + 1);
				let comma = if index + 1 < items.len() { 1 } else { 0 };
				write_pretty(out, item, options, depth + 1, indent_width(options, depth + 1), comma);
			}
			new_line(out, options, depth);
			out.push(']');
		}
		Value::Object(members) if !members.is_empty() => {
			if try_inline(out, value, options, used, trailing) {
				return;
			}

			out.push('{');
			for (index, (key, item)) in members.iter().enumerate() {
				if index > 0 {
					out.push(',');
				}
				new_line(out, options, depth + 1);

				let start = out.len();
				write_text(out, key, options);
				out.push_str(": ");
				// 幅は 1 行にまとめるかを決めるときにしか使わないので、max_width がなければ数えない
				let used = if options.max_width > 0 {
					indent_width(options, depth + 1).saturating_add(display_width(&out[start..]))
				} else {
					0
				};

				let comma = if index + 1 < members.len() { 1 } else { 0 };
				write_pretty(out, item, options, depth + 1, used, comma);
			}
			new_line(out, options, depth);
			out.push('}');
		}
		other => write_scalar_or_empty(out, other, options),
	}
}


/// max_width に収まるなら、1 行にして書き足して true を返す。収まらなければ何も書かない
fn try_inline(out: &mut String, value: &Value, options: &Options, used: usize, trailing: usize) -> bool {
	if options.max_width == 0 {
		return false;
	}
	let Some(mut room) = options.max_width.checked_sub(used.saturating_add(trailing)) else {
		return false;
	};

	let start = out.len();
	if write_inline(out, value, options, true, &mut room).is_err() {
		out.truncate(start);
		return false;
	}
	true
}


/// 改行せずに書く。spaced なら , と : の後ろに空白を入れる。
///
/// room は残りの桁数で、書いた分だけ減らす。足りなくなった時点で Err を返して打ち切る。
/// UNLIMITED なら幅を見ない。
fn write_inline(out: &mut String, value: &Value, options: &Options, spaced: bool, room: &mut usize) -> Result<(), ()> {
	let comma = if spaced { ", " } else { "," };
	let colon = if spaced { ": " } else { ":" };

	match value {
		Value::Array(items) if !items.is_empty() => {
			put(out, "[", room)?;
			for (index, item) in items.iter().enumerate() {
				if index > 0 {
					put(out, comma, room)?;
				}
				write_inline(out, item, options, spaced, room)?;
			}
			put(out, "]", room)?;
		}
		Value::Object(members) if !members.is_empty() => {
			put(out, "{", room)?;
			for (index, (key, item)) in members.iter().enumerate() {
				if index > 0 {
					put(out, comma, room)?;
				}
				put_text(out, key, options, room)?;
				put(out, colon, room)?;
				write_inline(out, item, options, spaced, room)?;
			}
			put(out, "}", room)?;
		}
		Value::String(text) => put_text(out, text, options, room)?,
		Value::Number(text) => put(out, text, room)?,
		Value::Null => put(out, "null", room)?,
		Value::Bool(true) => put(out, "true", room)?,
		Value::Bool(false) => put(out, "false", room)?,
		Value::Array(_) => put(out, "[]", room)?,
		Value::Object(_) => put(out, "{}", room)?,
	}
	Ok(())
}


/// ASCII だけの字句を書き、その幅だけ room を減らす。ASCII なのでバイト数がそのまま幅になる
fn put(out: &mut String, text: &str, room: &mut usize) -> Result<(), ()> {
	out.push_str(text);
	if *room == UNLIMITED {
		return Ok(());
	}
	if text.len() > *room {
		return Err(());
	}
	*room -= text.len();
	Ok(())
}


/// 文字列を書き、その幅だけ room を減らす。明らかに収まらない文字列は書かずに打ち切る
fn put_text(out: &mut String, text: &Cow<str>, options: &Options, room: &mut usize) -> Result<(), ()> {
	if *room == UNLIMITED {
		write_text(out, text, options);
		return Ok(());
	}

	// 1 文字は多くても 4 バイトで、少なくとも 1 桁になる。前後の " で 2 桁
	if text.len() / 4 + 2 > *room {
		return Err(());
	}

	let start = out.len();
	write_text(out, text, options);
	let width = display_width(&out[start..]);
	if width > *room {
		return Err(());
	}
	*room -= width;
	Ok(())
}


fn new_line(out: &mut String, options: &Options, depth: usize) {
	out.push('\n');
	match options.indent {
		Indent::Spaces(count) => push_repeated(out, SPACES, count.saturating_mul(depth)),
		Indent::Tab => push_repeated(out, TABS, depth),
		Indent::Compact => {}
	}
}


/// run(同じ 1 バイト文字の並び)から count 文字分を書く
fn push_repeated(out: &mut String, run: &str, count: usize) {
	let mut left = count;
	while left > run.len() {
		out.push_str(run);
		left -= run.len();
	}
	out.push_str(&run[..left]);
}


/// depth 段のインデントの見た目の桁数。あふれるほど大きければ usize::MAX で止める
fn indent_width(options: &Options, depth: usize) -> usize {
	match options.indent {
		Indent::Spaces(count) => count.saturating_mul(depth),
		Indent::Tab => TAB_WIDTH.saturating_mul(depth),
		Indent::Compact => 0,
	}
}


/// 見た目の桁数。全角(CJK、絵文字など)は 2 桁として数える。
fn display_width(text: &str) -> usize {
	text.chars().map(char_width).sum()
}


fn char_width(character: char) -> usize {
	let wide = matches!(character as u32,
		0x1100..=0x115F        // ハングル字母
		| 0x2E80..=0x303E      // CJK 部首、記号
		| 0x3041..=0x33FF      // かな、ハングル、CJK 互換
		| 0x3400..=0x4DBF      // CJK 拡張 A
		| 0x4E00..=0x9FFF      // CJK 統合漢字
		| 0xA000..=0xA4CF      // イ文字
		| 0xAC00..=0xD7A3      // ハングル音節
		| 0xF900..=0xFAFF      // CJK 互換漢字
		| 0xFE30..=0xFE6F      // CJK 互換形
		| 0xFF00..=0xFF60      // 全角英数
		| 0xFFE0..=0xFFE6      // 全角記号
		| 0x1F300..=0x1F9FF    // 絵文字
		| 0x20000..=0x3FFFD    // CJK 拡張 B 以降
	);
	if wide {
		2
	} else {
		1
	}
}


fn write_scalar_or_empty(out: &mut String, value: &Value, options: &Options) {
	match value {
		Value::Null => out.push_str("null"),
		Value::Bool(true) => out.push_str("true"),
		Value::Bool(false) => out.push_str("false"),
		Value::Number(text) => out.push_str(text),
		Value::String(text) => write_text(out, text, options),
		Value::Array(_) => out.push_str("[]"),
		Value::Object(_) => out.push_str("{}"),
	}
}


/// 文字列を書く。
///
/// 入力にエスケープがなかった文字列は、" も \ も制御文字も含まないので、入力の字面がそのまま正しい書き方になる。
/// ensure_ascii でなければ、それをそのまま写す。
fn write_text(out: &mut String, text: &Cow<str>, options: &Options) {
	match text {
		Cow::Borrowed(raw) if !options.ensure_ascii => {
			out.push('"');
			out.push_str(raw);
			out.push('"');
		}
		_ => write_string(out, text, options),
	}
}


/// 文字列をエスケープして書く。エスケープのいらない部分はまとめて写す
fn write_string(out: &mut String, text: &str, options: &Options) {
	out.push('"');
	let bytes = text.as_bytes();
	let mut start = 0;
	let mut index = 0;

	while index < bytes.len() {
		let byte = bytes[index];
		if byte >= 0x20 && byte != b'"' && byte != b'\\' && (byte < 0x80 || !options.ensure_ascii) {
			index += 1;
			continue;
		}

		out.push_str(&text[start..index]);
		match byte {
			b'"' => out.push_str("\\\""),
			b'\\' => out.push_str("\\\\"),
			b'\n' => out.push_str("\\n"),
			b'\r' => out.push_str("\\r"),
			b'\t' => out.push_str("\\t"),
			0x08 => out.push_str("\\b"),
			0x0C => out.push_str("\\f"),
			0x00..=0x1F => push_unit(out, byte as u16),
			_ => {
				// ensure_ascii のときだけ来る。index は文字の先頭にある。BMP の外はサロゲートペアの 2 つになる
				let Some(character) = text[index..].chars().next() else {
					break;
				};
				let mut units = [0; 2];
				for unit in character.encode_utf16(&mut units) {
					push_unit(out, *unit);
				}
				index += character.len_utf8();
				start = index;
				continue;
			}
		}
		index += 1;
		start = index;
	}

	out.push_str(&text[start..]);
	out.push('"');
}


/// \u と 16 進数 4 桁を書く。16 進数は小文字
fn push_unit(out: &mut String, unit: u16) {
	out.push_str("\\u");
	for shift in [12, 8, 4, 0] {
		out.push(HEX[((unit >> shift) & 0xF) as usize] as char);
	}
}