T: named lists continuing across stories (list.define, listName/startAt); null clears overrides
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6b362c5f71
commit
1b778945af
|
|
@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
|
||||
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
|
||||
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
|
||||
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
|
||||
- View › Overprint Preview (⌥⇧⌘Y): overprinting fills, strokes and 100% [Black] text mix with what's beneath on screen.
|
||||
- Type on a Path (⇧T tool, `type.onPath`): a story along any line, curve or shape on screen and in PDF; Type on a Path Options (start, flip, baseline/ascender/descender/centre) and Delete Type from Path. Round-trips through IDML as TextPath; on-path caret drawing is still to come.
|
||||
- Preferences › Appearance of Black: 100% K on screen accurately (dark grey) or as rich black (`window.richBlack`); the printing/export side is still to come.
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@ impl Cache {
|
|||
format!("{:?}", doc.footnote_options).hash(&mut h);
|
||||
sig.extend([doc.footnote_start(sid) as usize, h.finish() as usize]);
|
||||
}
|
||||
// Named lists continue from earlier stories.
|
||||
for l in &doc.settings.lists {
|
||||
sig.push(doc.list_start(sid, &l.name) as usize);
|
||||
}
|
||||
{
|
||||
let mut g = self.map.lock().unwrap_or_else(|e| e.into_inner());
|
||||
g.1 += 1;
|
||||
|
|
|
|||
|
|
@ -361,6 +361,23 @@ pub fn compose(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &Compo
|
|||
let para_ranges = story.para_ranges();
|
||||
let np = para_ranges.len();
|
||||
let mut list_counter: u32 = 0;
|
||||
// Named lists number independently of layout: one pass up front.
|
||||
let named_numbers: Vec<Option<u32>> = {
|
||||
let mut counters: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
|
||||
story
|
||||
.paras
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let (pp, _) = doc.styles.resolve_para(p);
|
||||
if pp.list_type != designcraft_doc::ListType::Numbers || pp.list_name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let c = counters.entry(pp.list_name.clone()).or_insert_with(|| doc.list_start(story.id, &pp.list_name));
|
||||
*c = pp.start_at.map_or(*c + 1, |s| s.max(1));
|
||||
Some(*c)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
// Keep options: paragraphs are re-laid from a snapshot when a keep is violated, either forced
|
||||
// into the next column or with a cap on the lines set before moving on (widow control).
|
||||
let mut snaps: Vec<Snapshot> = Vec::with_capacity(np);
|
||||
|
|
@ -465,8 +482,14 @@ pub fn compose(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &Compo
|
|||
&pp.grep_styles,
|
||||
);
|
||||
match pp.list_type {
|
||||
designcraft_doc::ListType::Numbers if !pp.list_name.is_empty() => {
|
||||
// A named list: carries on past other paragraphs (and from earlier stories).
|
||||
let n = named_numbers.get(pi).copied().flatten().unwrap_or(1);
|
||||
let label = format!("{}.{}", pp.number_style.format(n), pp.list_separator);
|
||||
prepend_label(db, &mut sp.glyphs, &label, prange.start, &base_chars, &pp, &mut table);
|
||||
}
|
||||
designcraft_doc::ListType::Numbers => {
|
||||
list_counter += 1;
|
||||
list_counter = pp.start_at.map_or(list_counter + 1, |s| s.max(1));
|
||||
let label = format!("{}.{}", pp.number_style.format(list_counter), pp.list_separator);
|
||||
prepend_label(db, &mut sp.glyphs, &label, prange.start, &base_chars, &pp, &mut table);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -506,6 +506,10 @@ attr_set! {
|
|||
rule_below: Rule = Rule::default(),
|
||||
tabs: Vec<TabStop> = Vec::new(),
|
||||
list_type: ListType = ListType::None,
|
||||
/// Named list (Define Lists) the numbers belong to ("" = the story's own list).
|
||||
list_name: String = String::new(),
|
||||
/// Start At: restart the numbering at this number.
|
||||
start_at: Option<u32> = None,
|
||||
bullet_char: String = "\u{2022}".into(),
|
||||
number_style: NumberStyle = NumberStyle::Arabic,
|
||||
/// Text after the number/bullet (`^t` = tab).
|
||||
|
|
|
|||
|
|
@ -116,6 +116,21 @@ impl Default for DocumentGrid {
|
|||
}
|
||||
}
|
||||
|
||||
/// A named numbered list.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct NumberedList {
|
||||
pub name: String,
|
||||
/// Numbering continues from story to story (in page order).
|
||||
pub continue_across_stories: bool,
|
||||
}
|
||||
|
||||
impl Default for NumberedList {
|
||||
fn default() -> Self {
|
||||
NumberedList { name: String::new(), continue_across_stories: true }
|
||||
}
|
||||
}
|
||||
|
||||
/// File → Document Setup + the guide/grid/unit preferences stored with the document.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
|
|
@ -139,6 +154,9 @@ pub struct DocSettings {
|
|||
pub bleed_color: [u8; 3],
|
||||
pub slug_color: [u8; 3],
|
||||
pub keyboard_increment: f64,
|
||||
/// Named lists (Type › Bulleted and Numbered Lists › Define Lists).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub lists: Vec<NumberedList>,
|
||||
/// The story in the primary text frames (Smart Text Reflow adds and removes pages for it).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub primary_story: Option<StoryId>,
|
||||
|
|
@ -167,6 +185,7 @@ impl Default for DocSettings {
|
|||
slug_color: [100, 188, 221],
|
||||
keyboard_increment: 1.0,
|
||||
primary_story: None,
|
||||
lists: Vec::new(),
|
||||
chapter_number: 1,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,6 +219,32 @@ impl Story {
|
|||
}
|
||||
|
||||
impl crate::Document {
|
||||
/// How many numbers of list `name` stories before `sid` used (page order), so `sid`'s numbering
|
||||
/// carries on — 0 for lists that don't continue across stories.
|
||||
pub fn list_start(&self, sid: StoryId, name: &str) -> u32 {
|
||||
if !self.settings.lists.iter().any(|l| l.name == name && l.continue_across_stories) {
|
||||
return 0;
|
||||
}
|
||||
let key = |st: &Story| -> (usize, u64) {
|
||||
let page = st.frames.first().and_then(|f| self.page_of_item(*f)).unwrap_or(usize::MAX);
|
||||
(page, st.id.0)
|
||||
};
|
||||
let Some(me) = self.story(sid) else { return 0 };
|
||||
let mine = key(me);
|
||||
let mut before: Vec<&Story> = self.stories.values().map(|s| s.as_ref()).filter(|st| st.id != sid && key(st) < mine).collect();
|
||||
before.sort_by_key(|st| key(st));
|
||||
let mut n = 0u32;
|
||||
for st in before {
|
||||
for p in &st.paras {
|
||||
let (pp, _) = self.styles.resolve_para(p);
|
||||
if pp.list_type == crate::ListType::Numbers && pp.list_name == name {
|
||||
n = pp.start_at.map_or(n + 1, |s| s.max(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// Number of the first footnote of story `sid` when numbering runs through the document:
|
||||
/// the start number plus the footnotes of stories that start on earlier pages.
|
||||
pub fn footnote_start(&self, sid: StoryId) -> u32 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
//! Type › Bulleted and Numbered Lists › Define Lists: named lists whose numbers can continue from
|
||||
//! story to story. Paragraphs join one with `listName` (and restart with `startAt`).
|
||||
|
||||
use designcraft_doc::NumberedList;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, bad, cmd, has_doc, str_param};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
"list.define",
|
||||
"Define Lists…",
|
||||
["Type", "Bulleted and Numbered Lists"],
|
||||
None,
|
||||
"{name, continueAcrossStories?: true, rename?} — paragraphs join it with type.para {listType: numbers, listName}",
|
||||
has_doc,
|
||||
|s, p| {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("list.define", "missing name"))?.to_string();
|
||||
let cont = p.get("continueAcrossStories").and_then(Value::as_bool);
|
||||
let rename = str_param(p, "rename").map(str::to_string);
|
||||
s.edit(|d, _| {
|
||||
let lists = &mut d.settings.lists;
|
||||
match lists.iter_mut().find(|l| l.name == name) {
|
||||
Some(l) => {
|
||||
if let Some(c) = cont {
|
||||
l.continue_across_stories = c;
|
||||
}
|
||||
if let Some(r) = &rename {
|
||||
l.name = r.clone();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
lists.push(NumberedList { name: rename.clone().unwrap_or(name.clone()), continue_across_stories: cont.unwrap_or(true) })
|
||||
}
|
||||
}
|
||||
Ok(json!({"lists": d.settings.lists}))
|
||||
})
|
||||
}
|
||||
),
|
||||
cmd!("list.delete", "Delete List", [], None, "{name}", has_doc, |s, p| {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("list.delete", "missing name"))?.to_string();
|
||||
s.edit(|d, _| {
|
||||
d.settings.lists.retain(|l| l.name != name);
|
||||
Ok(json!({"lists": d.settings.lists}))
|
||||
})
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// The digit the story's first line starts with (its list number).
|
||||
fn digit(g: &designcraft_compose::PlacedGlyph) -> char {
|
||||
let face = g.face.get();
|
||||
('0'..='9').find(|c| face.glyph_for(*c) == g.gid).unwrap_or('?')
|
||||
}
|
||||
|
||||
fn first_char(s: &Session, sid: u64) -> char {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let cs = designcraft_compose::compose_story(d, designcraft_doc::StoryId(sid), &Default::default());
|
||||
digit(&cs.frames[0].lines[0].glyphs[0])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_lists_continue_across_stories() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"pages": 2, "facingPages": false})).unwrap();
|
||||
s.execute("list.define", &json!({"name": "Steps"})).unwrap();
|
||||
let mut stories = vec![];
|
||||
for (spread, text) in [(0, "Mix\nStir"), (1, "Bake\nServe")] {
|
||||
let r = s.execute("frame.create", &json!({"spread": spread, "rect": [72, 72, 400, 300], "content": "text", "text": text})).unwrap();
|
||||
let sid = r["story"].as_u64().unwrap();
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": text.len()})).unwrap();
|
||||
s.execute("type.para", &json!({"attrs": {"listType": "numbers", "listName": "Steps"}})).unwrap();
|
||||
stories.push(sid);
|
||||
}
|
||||
assert_eq!(first_char(&s, stories[0]), '1');
|
||||
assert_eq!(first_char(&s, stories[1]), '3', "carries on from the first story");
|
||||
// Not continuing: each story starts at 1.
|
||||
s.execute("list.define", &json!({"name": "Steps", "continueAcrossStories": false})).unwrap();
|
||||
assert_eq!(first_char(&s, stories[1]), '1');
|
||||
s.execute("list.define", &json!({"name": "Steps", "continueAcrossStories": true})).unwrap();
|
||||
// The session's cache follows earlier stories (no stale numbers).
|
||||
s.execute("text.select", &json!({"story": stories[0], "anchor": 0, "focus": 0})).unwrap();
|
||||
s.execute("text.insert", &json!({"text": "Prep\n"})).unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let cs = s.cache.get(&d, designcraft_doc::StoryId(stories[1]), None);
|
||||
assert_eq!(digit(&cs.frames[0].lines[0].glyphs[0]), '4');
|
||||
// Start At restarts.
|
||||
s.execute("text.select", &json!({"story": stories[1], "anchor": 0, "focus": 0})).unwrap();
|
||||
s.execute("type.para", &json!({"attrs": {"startAt": 7}})).unwrap();
|
||||
assert_eq!(first_char(&s, stories[1]), '7');
|
||||
// null removes the override again.
|
||||
s.execute("type.para", &json!({"attrs": {"startAt": null}})).unwrap();
|
||||
assert_eq!(first_char(&s, stories[1]), '4');
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ mod interactive;
|
|||
pub mod interchange;
|
||||
mod layout;
|
||||
mod links;
|
||||
mod lists;
|
||||
mod notes;
|
||||
mod object;
|
||||
mod overrides;
|
||||
|
|
@ -131,6 +132,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
|||
v.extend(object::specs());
|
||||
v.extend(anchors::specs());
|
||||
v.extend(paths::specs());
|
||||
v.extend(lists::specs());
|
||||
v.extend(path_type::specs());
|
||||
v.extend(package::specs());
|
||||
v.extend(print::specs());
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@ pub(crate) fn format_chars(s: &mut Session, attrs: &Value) -> Result<Value> {
|
|||
a.set_json(k, v).map_err(|e| bad("type.char", e))?;
|
||||
}
|
||||
}
|
||||
let cleared: Vec<String> = attrs.as_object().map(|o| o.iter().filter(|(_, v)| v.is_null()).map(|(k, _)| k.clone()).collect()).unwrap_or_default();
|
||||
let targets = format_targets(s);
|
||||
s.edit(|d, _| {
|
||||
for t in &targets {
|
||||
|
|
@ -499,7 +500,12 @@ pub(crate) fn format_chars(s: &mut Session, attrs: &Value) -> Result<Value> {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
st.format_chars(r.clone(), |f| f.over.merge(&a));
|
||||
st.format_chars(r.clone(), |f| {
|
||||
f.over.merge(&a);
|
||||
for k in &cleared {
|
||||
let _ = f.over.set_json(k, &Value::Null);
|
||||
}
|
||||
});
|
||||
}
|
||||
ok()
|
||||
})
|
||||
|
|
@ -512,11 +518,18 @@ pub(crate) fn format_paras(s: &mut Session, attrs: &Value) -> Result<Value> {
|
|||
a.set_json(k, v).map_err(|e| bad("type.para", e))?;
|
||||
}
|
||||
}
|
||||
// `null` removes the override (back to the style's value).
|
||||
let cleared: Vec<String> = attrs.as_object().map(|o| o.iter().filter(|(_, v)| v.is_null()).map(|(k, _)| k.clone()).collect()).unwrap_or_default();
|
||||
let targets = format_targets(s);
|
||||
s.edit(|d, _| {
|
||||
for t in &targets {
|
||||
if let Some(st) = d.text_story_mut(t.story, t.cell) {
|
||||
st.format_paras(t.range.clone(), |p| p.para.merge(&a));
|
||||
st.format_paras(t.range.clone(), |p| {
|
||||
p.para.merge(&a);
|
||||
for k in &cleared {
|
||||
let _ = p.para.set_json(k, &Value::Null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
ok()
|
||||
|
|
|
|||
|
|
@ -235,3 +235,19 @@ fn newer_features_round_trip_through_idml() {
|
|||
assert_eq!((pt.0.start, pt.0.flip, pt.0.align), (12.0, true, designcraft_doc::PathAlign::Center));
|
||||
assert_eq!(back.story(pt.1).unwrap().text, "On the line");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_lists_round_trip_through_idml() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("list.define", &json!({"name": "Steps", "continueAcrossStories": false})).unwrap();
|
||||
let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": "One\nTwo"})).unwrap();
|
||||
s.execute("text.select", &json!({"story": r["story"], "anchor": 0, "focus": 3})).unwrap();
|
||||
s.execute("type.para", &json!({"attrs": {"listType": "numbers", "listName": "Steps", "startAt": 5}})).unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let back = designcraft_idml::import_idml(&designcraft_idml::export_idml(&d)).unwrap();
|
||||
assert_eq!(back.settings.lists, vec![designcraft_doc::NumberedList { name: "Steps".into(), continue_across_stories: false }]);
|
||||
let st = back.stories.values().find(|st| st.text.starts_with("One")).unwrap();
|
||||
assert_eq!(st.paras[0].para.list_name.as_deref(), Some("Steps"));
|
||||
assert_eq!(st.paras[0].para.start_at, Some(Some(5)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -820,6 +820,16 @@ impl<'a> Ex<'a> {
|
|||
tables = tables.child(with_props(el, vec![p("BasedOn", "string", "$ID/[Basic Table]")]));
|
||||
}
|
||||
root.push(tables);
|
||||
// Named numbered lists.
|
||||
for l in &self.d.settings.lists {
|
||||
root.push(
|
||||
El::new("NumberingList")
|
||||
.attr("Self", format!("NumberingList/{}", escape_id(&l.name)))
|
||||
.attr("Name", &l.name)
|
||||
.attr("ContinueNumbersAcrossStories", bool_s(l.continue_across_stories))
|
||||
.attr("ContinueNumbersAcrossDocuments", "false"),
|
||||
);
|
||||
}
|
||||
|
||||
// Object styles.
|
||||
let mut og = Group::default();
|
||||
|
|
@ -973,6 +983,21 @@ impl<'a> Ex<'a> {
|
|||
}
|
||||
|
||||
fn para_attrs(&mut self, el: &mut El, props: &mut Vec<El>, a: &ParaAttrs) {
|
||||
if let Some(n) = &a.list_name {
|
||||
el.set(
|
||||
"AppliedNumberingList",
|
||||
if n.is_empty() { "NumberingList/$ID/[Default]".to_string() } else { format!("NumberingList/{}", escape_id(n)) },
|
||||
);
|
||||
}
|
||||
if let Some(v) = a.start_at {
|
||||
match v {
|
||||
Some(n) => {
|
||||
el.set("NumberingContinue", "false");
|
||||
el.set("NumberingStartAt", n);
|
||||
}
|
||||
None => el.set("NumberingContinue", "true"),
|
||||
}
|
||||
}
|
||||
// Nested and GREP styles: property lists of records.
|
||||
if let Some(list) = &a.nested_styles {
|
||||
let mut l = El::new("AllNestedStyles").attr("type", "list");
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ struct Importer<'r> {
|
|||
styles: Styles,
|
||||
para_names: HashMap<String, String>,
|
||||
char_names: HashMap<String, String>,
|
||||
lists: Vec<designcraft_doc::NumberedList>,
|
||||
object_names: HashMap<String, String>,
|
||||
/// Object style Self → element (attribute fallback for page items).
|
||||
object_els: HashMap<String, El>,
|
||||
|
|
@ -177,6 +178,7 @@ impl<'r> Importer<'r> {
|
|||
styles,
|
||||
para_names: HashMap::new(),
|
||||
char_names: HashMap::new(),
|
||||
lists: Vec::new(),
|
||||
object_names: HashMap::new(),
|
||||
object_els: HashMap::new(),
|
||||
layers: Vec::new(),
|
||||
|
|
@ -595,6 +597,13 @@ impl<'r> Importer<'r> {
|
|||
// ---------- styles ----------
|
||||
|
||||
fn styles(&mut self, top: &[El]) {
|
||||
for e in top.iter().filter(|e| e.local() == "NumberingList") {
|
||||
let name = e.get("Name").unwrap_or("").to_string();
|
||||
if !name.is_empty() && !name.starts_with("$ID/") && !self.lists.iter().any(|l| l.name == name) {
|
||||
self.lists
|
||||
.push(designcraft_doc::NumberedList { name, continue_across_stories: e.get("ContinueNumbersAcrossStories") != Some("false") });
|
||||
}
|
||||
}
|
||||
let mut paras: Vec<(String, El)> = Vec::new();
|
||||
let mut chars: Vec<(String, El)> = Vec::new();
|
||||
let mut objects: Vec<(String, El)> = Vec::new();
|
||||
|
|
@ -988,6 +997,15 @@ impl<'r> Importer<'r> {
|
|||
a.first_line_indent = e.num("FirstLineIndent");
|
||||
a.last_line_indent = e.num("LastLineIndent");
|
||||
a.space_before = e.num("SpaceBefore");
|
||||
if let Some(r) = e.prop("AppliedNumberingList") {
|
||||
let r = r.trim().trim_start_matches("NumberingList/");
|
||||
a.list_name = Some(if r.starts_with("$ID/") { String::new() } else { unescape_id(r) });
|
||||
}
|
||||
match e.prop("NumberingContinue").as_deref().map(str::trim) {
|
||||
Some("false") => a.start_at = Some(e.num("NumberingStartAt").map(|n| n.max(1.0) as u32)),
|
||||
Some("true") => a.start_at = Some(None),
|
||||
_ => {}
|
||||
}
|
||||
// Nested and GREP styles.
|
||||
if let Some(l) = e.prop_el("AllNestedStyles") {
|
||||
let v: Vec<designcraft_doc::NestedStyle> = l
|
||||
|
|
@ -1903,6 +1921,7 @@ impl<'r> Importer<'r> {
|
|||
if self.spreads.is_empty() {
|
||||
return Err(IdmlError::Invalid("document has no spreads".into()));
|
||||
}
|
||||
self.settings.lists = std::mem::take(&mut self.lists);
|
||||
// Make sure built-in paragraph styles exist and are first.
|
||||
let d = Document {
|
||||
title,
|
||||
|
|
|
|||
|
|
@ -177,6 +177,9 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
|||
"ui:app.findFontDialog",
|
||||
"-",
|
||||
"cmd:type.createOutlines",
|
||||
">Bulleted and Numbered Lists",
|
||||
"cmd:list.define",
|
||||
"<",
|
||||
">Type on a Path",
|
||||
"cmd:type.pathOptions",
|
||||
"cmd:type.pathOptions|Delete Type from Path|{\"delete\": true}",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 64 | 25 | 8 | 79% |
|
||||
| P1 | 97 | 65 | 25 | 7 | 80% |
|
||||
| P2 | 76 | 8 | 7 | 61 | 15% |
|
||||
|
||||
| Area | Parity |
|
||||
|
|
@ -27,7 +27,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Typography | 85% |
|
||||
| Styles | 92% |
|
||||
| Tables | 81% |
|
||||
| Long documents | 58% |
|
||||
| Long documents | 75% |
|
||||
| Interactivity & digital | 30% |
|
||||
| Output & production | 68% |
|
||||
| XML & automation | 39% |
|
||||
|
|
@ -263,7 +263,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
|||
| Long documents | Table of contents | P1 | D |
|
||||
| Long documents | Index | P2 | D |
|
||||
| Long documents | Books | P2 | M |
|
||||
| Long documents | Numbered lists across stories | P1 | M |
|
||||
| Long documents | Numbered lists across stories | P1 | D |
|
||||
| Long documents | Cross-references | P2 | D |
|
||||
| Long documents | Running headers via variables | P1 | D |
|
||||
| Long documents | Footnotes/endnotes | P1 | P |
|
||||
|
|
|
|||
Loading…
Reference in New Issue