feat: artifact presentation part 7 (#13360)
This commit is contained in:
parent
1df040e62b
commit
24ba01b9da
10 changed files with 2883 additions and 220 deletions
|
|
@ -225,7 +225,7 @@ struct PartialPositionArgs {
|
|||
flip_vertical: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
struct TextStylingArgs {
|
||||
style: Option<String>,
|
||||
font_size: Option<u32>,
|
||||
|
|
@ -238,6 +238,57 @@ struct TextStylingArgs {
|
|||
underline: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
struct TextLayoutArgs {
|
||||
insets: Option<TextInsetsArgs>,
|
||||
wrap: Option<String>,
|
||||
auto_fit: Option<String>,
|
||||
vertical_alignment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct TextInsetsArgs {
|
||||
left: u32,
|
||||
right: u32,
|
||||
top: u32,
|
||||
bottom: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RichTextInput {
|
||||
Plain(String),
|
||||
Paragraphs(Vec<RichParagraphInput>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RichParagraphInput {
|
||||
Plain(String),
|
||||
Runs(Vec<RichRunInput>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RichRunInput {
|
||||
Plain(String),
|
||||
Styled(RichRunObjectInput),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct RichRunObjectInput {
|
||||
run: String,
|
||||
#[serde(default)]
|
||||
text_style: TextStylingArgs,
|
||||
link: Option<RichTextLinkInput>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct RichTextLinkInput {
|
||||
uri: Option<String>,
|
||||
is_external: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AddTextShapeArgs {
|
||||
slide_index: u32,
|
||||
|
|
@ -245,6 +296,8 @@ struct AddTextShapeArgs {
|
|||
position: PositionArgs,
|
||||
#[serde(flatten)]
|
||||
styling: TextStylingArgs,
|
||||
#[serde(default)]
|
||||
text_layout: TextLayoutArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
|
|
@ -267,6 +320,8 @@ struct AddShapeArgs {
|
|||
flip_vertical: Option<bool>,
|
||||
#[serde(default)]
|
||||
text_style: TextStylingArgs,
|
||||
#[serde(default)]
|
||||
text_layout: TextLayoutArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
|
|
@ -276,7 +331,7 @@ struct ConnectorLineArgs {
|
|||
style: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PointArgs {
|
||||
left: u32,
|
||||
top: u32,
|
||||
|
|
@ -355,6 +410,9 @@ struct AddTableArgs {
|
|||
column_widths: Option<Vec<u32>>,
|
||||
row_heights: Option<Vec<u32>>,
|
||||
style: Option<String>,
|
||||
style_options: Option<TableStyleOptionsArgs>,
|
||||
borders: Option<TableBordersArgs>,
|
||||
right_to_left: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -365,12 +423,55 @@ struct AddChartArgs {
|
|||
categories: Vec<String>,
|
||||
series: Vec<ChartSeriesArgs>,
|
||||
title: Option<String>,
|
||||
style_index: Option<u32>,
|
||||
has_legend: Option<bool>,
|
||||
legend_position: Option<String>,
|
||||
#[serde(default)]
|
||||
legend_text_style: TextStylingArgs,
|
||||
x_axis_title: Option<String>,
|
||||
y_axis_title: Option<String>,
|
||||
data_labels: Option<ChartDataLabelsArgs>,
|
||||
chart_fill: Option<String>,
|
||||
plot_area_fill: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChartSeriesArgs {
|
||||
name: String,
|
||||
values: Vec<f64>,
|
||||
categories: Option<Vec<String>>,
|
||||
x_values: Option<Vec<f64>>,
|
||||
fill: Option<String>,
|
||||
stroke: Option<StrokeArgs>,
|
||||
marker: Option<ChartMarkerArgs>,
|
||||
data_label_overrides: Option<Vec<ChartDataLabelOverrideArgs>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct ChartMarkerArgs {
|
||||
symbol: Option<String>,
|
||||
size: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct ChartDataLabelsArgs {
|
||||
show_value: Option<bool>,
|
||||
show_category_name: Option<bool>,
|
||||
show_leader_lines: Option<bool>,
|
||||
position: Option<String>,
|
||||
#[serde(default)]
|
||||
text_style: TextStylingArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct ChartDataLabelOverrideArgs {
|
||||
idx: u32,
|
||||
text: Option<String>,
|
||||
position: Option<String>,
|
||||
#[serde(default)]
|
||||
text_style: TextStylingArgs,
|
||||
fill: Option<String>,
|
||||
stroke: Option<StrokeArgs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -379,6 +480,43 @@ struct UpdateTextArgs {
|
|||
text: String,
|
||||
#[serde(default)]
|
||||
styling: TextStylingArgs,
|
||||
#[serde(default)]
|
||||
text_layout: TextLayoutArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SetRichTextArgs {
|
||||
element_id: Option<String>,
|
||||
slide_index: Option<u32>,
|
||||
row: Option<u32>,
|
||||
column: Option<u32>,
|
||||
notes: Option<bool>,
|
||||
text: RichTextInput,
|
||||
#[serde(default)]
|
||||
styling: TextStylingArgs,
|
||||
#[serde(default)]
|
||||
text_layout: TextLayoutArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FormatTextRangeArgs {
|
||||
element_id: Option<String>,
|
||||
slide_index: Option<u32>,
|
||||
row: Option<u32>,
|
||||
column: Option<u32>,
|
||||
notes: Option<bool>,
|
||||
query: Option<String>,
|
||||
occurrence: Option<usize>,
|
||||
start_cp: Option<usize>,
|
||||
length: Option<usize>,
|
||||
#[serde(default)]
|
||||
styling: TextStylingArgs,
|
||||
#[serde(default)]
|
||||
text_layout: TextLayoutArgs,
|
||||
link: Option<RichTextLinkInput>,
|
||||
spacing_before: Option<u32>,
|
||||
spacing_after: Option<u32>,
|
||||
line_spacing: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -422,6 +560,8 @@ struct UpdateShapeStyleArgs {
|
|||
crop: Option<ImageCropArgs>,
|
||||
lock_aspect_ratio: Option<bool>,
|
||||
z_order: Option<u32>,
|
||||
#[serde(default)]
|
||||
text_layout: TextLayoutArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -458,6 +598,55 @@ struct UpdateTableCellArgs {
|
|||
alignment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct TableStyleOptionsArgs {
|
||||
header_row: Option<bool>,
|
||||
banded_rows: Option<bool>,
|
||||
banded_columns: Option<bool>,
|
||||
first_column: Option<bool>,
|
||||
last_column: Option<bool>,
|
||||
total_row: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct TableBorderArgs {
|
||||
color: String,
|
||||
width: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct TableBordersArgs {
|
||||
outside: Option<TableBorderArgs>,
|
||||
inside: Option<TableBorderArgs>,
|
||||
top: Option<TableBorderArgs>,
|
||||
bottom: Option<TableBorderArgs>,
|
||||
left: Option<TableBorderArgs>,
|
||||
right: Option<TableBorderArgs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UpdateTableStyleArgs {
|
||||
element_id: String,
|
||||
style: Option<String>,
|
||||
style_options: Option<TableStyleOptionsArgs>,
|
||||
borders: Option<TableBordersArgs>,
|
||||
right_to_left: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct StyleTableBlockArgs {
|
||||
element_id: String,
|
||||
row: u32,
|
||||
column: u32,
|
||||
row_count: u32,
|
||||
column_count: u32,
|
||||
#[serde(default)]
|
||||
styling: TextStylingArgs,
|
||||
background_fill: Option<String>,
|
||||
alignment: Option<String>,
|
||||
borders: Option<TableBordersArgs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MergeTableCellsArgs {
|
||||
element_id: String,
|
||||
|
|
@ -466,3 +655,75 @@ struct MergeTableCellsArgs {
|
|||
start_column: u32,
|
||||
end_column: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UpdateChartArgs {
|
||||
element_id: String,
|
||||
title: Option<String>,
|
||||
categories: Option<Vec<String>>,
|
||||
style_index: Option<u32>,
|
||||
has_legend: Option<bool>,
|
||||
legend_position: Option<String>,
|
||||
#[serde(default)]
|
||||
legend_text_style: TextStylingArgs,
|
||||
x_axis_title: Option<String>,
|
||||
y_axis_title: Option<String>,
|
||||
data_labels: Option<ChartDataLabelsArgs>,
|
||||
chart_fill: Option<String>,
|
||||
plot_area_fill: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AddChartSeriesArgs {
|
||||
element_id: String,
|
||||
name: String,
|
||||
values: Vec<f64>,
|
||||
categories: Option<Vec<String>>,
|
||||
x_values: Option<Vec<f64>>,
|
||||
fill: Option<String>,
|
||||
stroke: Option<StrokeArgs>,
|
||||
marker: Option<ChartMarkerArgs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SetCommentAuthorArgs {
|
||||
display_name: String,
|
||||
initials: String,
|
||||
email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct CommentPositionArgs {
|
||||
x: u32,
|
||||
y: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AddCommentThreadArgs {
|
||||
slide_index: Option<u32>,
|
||||
element_id: Option<String>,
|
||||
query: Option<String>,
|
||||
occurrence: Option<usize>,
|
||||
start_cp: Option<usize>,
|
||||
length: Option<usize>,
|
||||
text: String,
|
||||
position: Option<CommentPositionArgs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AddCommentReplyArgs {
|
||||
thread_id: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ToggleCommentReactionArgs {
|
||||
thread_id: String,
|
||||
message_id: Option<String>,
|
||||
emoji: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CommentThreadIdArgs {
|
||||
thread_id: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ fn inspect_document(document: &PresentationDocument, args: &InspectArgs) -> Stri
|
|||
.include
|
||||
.as_deref()
|
||||
.or(args.kind.as_deref())
|
||||
.unwrap_or("deck,slide,textbox,shape,connector,table,chart,image,notes,layoutList");
|
||||
.unwrap_or(
|
||||
"deck,slide,textbox,shape,connector,table,chart,image,notes,layoutList,textRange,comment",
|
||||
);
|
||||
let included_kinds = include_kinds
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
|
|
@ -33,6 +35,11 @@ fn inspect_document(document: &PresentationDocument, args: &InspectArgs) -> Stri
|
|||
.collect::<Vec<_>>(),
|
||||
"activeSlideIndex": document.active_slide_index,
|
||||
"activeSlideId": document.active_slide_index.and_then(|index| document.slides.get(index)).map(|slide| format!("sl/{}", slide.slide_id)),
|
||||
"commentThreadIds": document
|
||||
.comment_threads
|
||||
.iter()
|
||||
.map(|thread| format!("th/{}", thread.thread_id))
|
||||
.collect::<Vec<_>>(),
|
||||
}),
|
||||
None,
|
||||
));
|
||||
|
|
@ -97,10 +104,29 @@ fn inspect_document(document: &PresentationDocument, args: &InspectArgs) -> Stri
|
|||
"textPreview": slide.notes.text.replace('\n', " | "),
|
||||
"textChars": slide.notes.text.chars().count(),
|
||||
"textLines": slide.notes.text.lines().count(),
|
||||
"richText": rich_text_to_proto(&slide.notes.text, &slide.notes.rich_text),
|
||||
}),
|
||||
Some(slide_id.clone()),
|
||||
));
|
||||
}
|
||||
if include("textRange") {
|
||||
records.extend(
|
||||
slide
|
||||
.notes
|
||||
.rich_text
|
||||
.ranges
|
||||
.iter()
|
||||
.map(|range| {
|
||||
let mut record = text_range_to_proto(&slide.notes.text, range);
|
||||
record["kind"] = Value::String("textRange".to_string());
|
||||
record["slide"] = Value::from(index + 1);
|
||||
record["slideIndex"] = Value::from(index);
|
||||
record["hostAnchor"] = Value::String(format!("nt/{}", slide.slide_id));
|
||||
record["hostKind"] = Value::String("notes".to_string());
|
||||
(record, Some(slide_id.clone()))
|
||||
}),
|
||||
);
|
||||
}
|
||||
for element in &slide.elements {
|
||||
let mut record = match element {
|
||||
PresentationElement::Text(text) => {
|
||||
|
|
@ -116,6 +142,7 @@ fn inspect_document(document: &PresentationDocument, args: &InspectArgs) -> Stri
|
|||
"textPreview": text.text.replace('\n', " | "),
|
||||
"textChars": text.text.chars().count(),
|
||||
"textLines": text.text.lines().count(),
|
||||
"richText": rich_text_to_proto(&text.text, &text.rich_text),
|
||||
"bbox": [text.frame.left, text.frame.top, text.frame.width, text.frame.height],
|
||||
"bboxUnit": "points",
|
||||
})
|
||||
|
|
@ -136,6 +163,12 @@ fn inspect_document(document: &PresentationDocument, args: &InspectArgs) -> Stri
|
|||
"geometry": format!("{:?}", shape.geometry),
|
||||
"text": shape.text,
|
||||
"textStyle": text_style_to_proto(&shape.text_style),
|
||||
"richText": shape
|
||||
.text
|
||||
.as_ref()
|
||||
.zip(shape.rich_text.as_ref())
|
||||
.map(|(text, rich_text)| rich_text_to_proto(text, rich_text))
|
||||
.unwrap_or(Value::Null),
|
||||
"rotation": shape.rotation_degrees,
|
||||
"flipHorizontal": shape.flip_horizontal,
|
||||
"flipVertical": shape.flip_vertical,
|
||||
|
|
@ -178,11 +211,19 @@ fn inspect_document(document: &PresentationDocument, args: &InspectArgs) -> Stri
|
|||
"rowHeights": table.row_heights,
|
||||
"preview": table.rows.first().map(|row| row.iter().map(|cell| cell.text.clone()).collect::<Vec<_>>().join(" | ")),
|
||||
"style": table.style,
|
||||
"styleOptions": table_style_options_to_proto(&table.style_options),
|
||||
"borders": table.borders.as_ref().map(table_borders_to_proto),
|
||||
"rightToLeft": table.right_to_left,
|
||||
"cellTextStyles": table
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| row.iter().map(|cell| text_style_to_proto(&cell.text_style)).collect::<Vec<_>>())
|
||||
.collect::<Vec<_>>(),
|
||||
"rowsData": table
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| row.iter().map(table_cell_to_proto).collect::<Vec<_>>())
|
||||
.collect::<Vec<_>>(),
|
||||
"bbox": [table.frame.left, table.frame.top, table.frame.width, table.frame.height],
|
||||
"bboxUnit": "points",
|
||||
})
|
||||
|
|
@ -197,6 +238,32 @@ fn inspect_document(document: &PresentationDocument, args: &InspectArgs) -> Stri
|
|||
"slide": index + 1,
|
||||
"chartType": format!("{:?}", chart.chart_type),
|
||||
"title": chart.title,
|
||||
"styleIndex": chart.style_index,
|
||||
"hasLegend": chart.has_legend,
|
||||
"legend": chart.legend.as_ref().map(chart_legend_to_proto),
|
||||
"xAxis": chart.x_axis.as_ref().map(chart_axis_to_proto),
|
||||
"yAxis": chart.y_axis.as_ref().map(chart_axis_to_proto),
|
||||
"dataLabels": chart.data_labels.as_ref().map(chart_data_labels_to_proto),
|
||||
"chartFill": chart.chart_fill,
|
||||
"plotAreaFill": chart.plot_area_fill,
|
||||
"series": chart
|
||||
.series
|
||||
.iter()
|
||||
.map(|series| serde_json::json!({
|
||||
"name": series.name,
|
||||
"values": series.values,
|
||||
"categories": series.categories,
|
||||
"xValues": series.x_values,
|
||||
"fill": series.fill,
|
||||
"stroke": series.stroke.as_ref().map(stroke_to_proto),
|
||||
"marker": series.marker.as_ref().map(chart_marker_to_proto),
|
||||
"dataLabelOverrides": series
|
||||
.data_label_overrides
|
||||
.iter()
|
||||
.map(chart_data_label_override_to_proto)
|
||||
.collect::<Vec<_>>(),
|
||||
}))
|
||||
.collect::<Vec<_>>(),
|
||||
"bbox": [chart.frame.left, chart.frame.top, chart.frame.width, chart.frame.height],
|
||||
"bboxUnit": "points",
|
||||
})
|
||||
|
|
@ -261,8 +328,64 @@ fn inspect_document(document: &PresentationDocument, args: &InspectArgs) -> Stri
|
|||
record["hyperlink"] = hyperlink.to_json();
|
||||
}
|
||||
records.push((record, Some(slide_id.clone())));
|
||||
if include("textRange") {
|
||||
match element {
|
||||
PresentationElement::Text(text) => {
|
||||
records.extend(text.rich_text.ranges.iter().map(|range| {
|
||||
let mut record = text_range_to_proto(&text.text, range);
|
||||
record["kind"] = Value::String("textRange".to_string());
|
||||
record["slide"] = Value::from(index + 1);
|
||||
record["slideIndex"] = Value::from(index);
|
||||
record["hostAnchor"] = Value::String(format!("sh/{}", text.element_id));
|
||||
record["hostKind"] = Value::String("textbox".to_string());
|
||||
(record, Some(slide_id.clone()))
|
||||
}));
|
||||
}
|
||||
PresentationElement::Shape(shape) => {
|
||||
if let Some((text, rich_text)) = shape.text.as_ref().zip(shape.rich_text.as_ref()) {
|
||||
records.extend(rich_text.ranges.iter().map(|range| {
|
||||
let mut record = text_range_to_proto(text, range);
|
||||
record["kind"] = Value::String("textRange".to_string());
|
||||
record["slide"] = Value::from(index + 1);
|
||||
record["slideIndex"] = Value::from(index);
|
||||
record["hostAnchor"] = Value::String(format!("sh/{}", shape.element_id));
|
||||
record["hostKind"] = Value::String("textbox".to_string());
|
||||
(record, Some(slide_id.clone()))
|
||||
}));
|
||||
}
|
||||
}
|
||||
PresentationElement::Table(table) => {
|
||||
for (row_index, row) in table.rows.iter().enumerate() {
|
||||
for (column_index, cell) in row.iter().enumerate() {
|
||||
records.extend(cell.rich_text.ranges.iter().map(|range| {
|
||||
let mut record = text_range_to_proto(&cell.text, range);
|
||||
record["kind"] = Value::String("textRange".to_string());
|
||||
record["slide"] = Value::from(index + 1);
|
||||
record["slideIndex"] = Value::from(index);
|
||||
record["hostAnchor"] = Value::String(format!(
|
||||
"tb/{}#cell/{row_index}/{column_index}",
|
||||
table.element_id
|
||||
));
|
||||
record["hostKind"] = Value::String("tableCell".to_string());
|
||||
(record, Some(slide_id.clone()))
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
PresentationElement::Connector(_)
|
||||
| PresentationElement::Image(_)
|
||||
| PresentationElement::Chart(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if include("comment") {
|
||||
records.extend(document.comment_threads.iter().map(|thread| {
|
||||
let mut record = comment_thread_to_proto(thread);
|
||||
record["id"] = Value::String(format!("th/{}", thread.thread_id));
|
||||
(record, None)
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(target_id) = args.target_id.as_deref() {
|
||||
records.retain(|(record, slide_id)| {
|
||||
|
|
@ -442,6 +565,27 @@ fn resolve_anchor(
|
|||
"text": slide.notes.text,
|
||||
});
|
||||
add_text_metadata(&mut record, &slide.notes.text);
|
||||
record["richText"] = rich_text_to_proto(&slide.notes.text, &slide.notes.rich_text);
|
||||
return Ok(record);
|
||||
}
|
||||
if let Some(range_id) = id.strip_prefix("tr/")
|
||||
&& let Some(record) = slide
|
||||
.notes
|
||||
.rich_text
|
||||
.ranges
|
||||
.iter()
|
||||
.find(|range| range.range_id == range_id)
|
||||
.map(|range| {
|
||||
let mut record = text_range_to_proto(&slide.notes.text, range);
|
||||
record["kind"] = Value::String("textRange".to_string());
|
||||
record["id"] = Value::String(id.to_string());
|
||||
record["slide"] = Value::from(slide_index + 1);
|
||||
record["slideIndex"] = Value::from(slide_index);
|
||||
record["hostAnchor"] = Value::String(notes_id.clone());
|
||||
record["hostKind"] = Value::String("notes".to_string());
|
||||
record
|
||||
})
|
||||
{
|
||||
return Ok(record);
|
||||
}
|
||||
for element in &slide.elements {
|
||||
|
|
@ -455,6 +599,7 @@ fn resolve_anchor(
|
|||
"slideIndex": slide_index,
|
||||
"text": text.text,
|
||||
"textStyle": text_style_to_proto(&text.style),
|
||||
"richText": rich_text_to_proto(&text.text, &text.rich_text),
|
||||
"bbox": [text.frame.left, text.frame.top, text.frame.width, text.frame.height],
|
||||
"bboxUnit": "points",
|
||||
});
|
||||
|
|
@ -471,6 +616,12 @@ fn resolve_anchor(
|
|||
"geometry": format!("{:?}", shape.geometry),
|
||||
"text": shape.text,
|
||||
"textStyle": text_style_to_proto(&shape.text_style),
|
||||
"richText": shape
|
||||
.text
|
||||
.as_ref()
|
||||
.zip(shape.rich_text.as_ref())
|
||||
.map(|(text, rich_text)| rich_text_to_proto(text, rich_text))
|
||||
.unwrap_or(Value::Null),
|
||||
"rotation": shape.rotation_degrees,
|
||||
"flipHorizontal": shape.flip_horizontal,
|
||||
"flipVertical": shape.flip_vertical,
|
||||
|
|
@ -527,11 +678,20 @@ fn resolve_anchor(
|
|||
"cols": table.rows.iter().map(std::vec::Vec::len).max().unwrap_or(0),
|
||||
"columnWidths": table.column_widths,
|
||||
"rowHeights": table.row_heights,
|
||||
"style": table.style,
|
||||
"styleOptions": table_style_options_to_proto(&table.style_options),
|
||||
"borders": table.borders.as_ref().map(table_borders_to_proto),
|
||||
"rightToLeft": table.right_to_left,
|
||||
"cellTextStyles": table
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| row.iter().map(|cell| text_style_to_proto(&cell.text_style)).collect::<Vec<_>>())
|
||||
.collect::<Vec<_>>(),
|
||||
"rowsData": table
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| row.iter().map(table_cell_to_proto).collect::<Vec<_>>())
|
||||
.collect::<Vec<_>>(),
|
||||
"bbox": [table.frame.left, table.frame.top, table.frame.width, table.frame.height],
|
||||
"bboxUnit": "points",
|
||||
}),
|
||||
|
|
@ -543,6 +703,32 @@ fn resolve_anchor(
|
|||
"slideIndex": slide_index,
|
||||
"chartType": format!("{:?}", chart.chart_type),
|
||||
"title": chart.title,
|
||||
"styleIndex": chart.style_index,
|
||||
"hasLegend": chart.has_legend,
|
||||
"legend": chart.legend.as_ref().map(chart_legend_to_proto),
|
||||
"xAxis": chart.x_axis.as_ref().map(chart_axis_to_proto),
|
||||
"yAxis": chart.y_axis.as_ref().map(chart_axis_to_proto),
|
||||
"dataLabels": chart.data_labels.as_ref().map(chart_data_labels_to_proto),
|
||||
"chartFill": chart.chart_fill,
|
||||
"plotAreaFill": chart.plot_area_fill,
|
||||
"series": chart
|
||||
.series
|
||||
.iter()
|
||||
.map(|series| serde_json::json!({
|
||||
"name": series.name,
|
||||
"values": series.values,
|
||||
"categories": series.categories,
|
||||
"xValues": series.x_values,
|
||||
"fill": series.fill,
|
||||
"stroke": series.stroke.as_ref().map(stroke_to_proto),
|
||||
"marker": series.marker.as_ref().map(chart_marker_to_proto),
|
||||
"dataLabelOverrides": series
|
||||
.data_label_overrides
|
||||
.iter()
|
||||
.map(chart_data_label_override_to_proto)
|
||||
.collect::<Vec<_>>(),
|
||||
}))
|
||||
.collect::<Vec<_>>(),
|
||||
"bbox": [chart.frame.left, chart.frame.top, chart.frame.width, chart.frame.height],
|
||||
"bboxUnit": "points",
|
||||
}),
|
||||
|
|
@ -582,9 +768,84 @@ fn resolve_anchor(
|
|||
if record.get("id").and_then(Value::as_str) == Some(id) {
|
||||
return Ok(record);
|
||||
}
|
||||
if let Some(range_id) = id.strip_prefix("tr/") {
|
||||
match element {
|
||||
PresentationElement::Text(text) => {
|
||||
if let Some(range) =
|
||||
text.rich_text.ranges.iter().find(|range| range.range_id == range_id)
|
||||
{
|
||||
let mut range_record = text_range_to_proto(&text.text, range);
|
||||
range_record["kind"] = Value::String("textRange".to_string());
|
||||
range_record["id"] = Value::String(id.to_string());
|
||||
range_record["slide"] = Value::from(slide_index + 1);
|
||||
range_record["slideIndex"] = Value::from(slide_index);
|
||||
range_record["hostAnchor"] =
|
||||
Value::String(format!("sh/{}", text.element_id));
|
||||
range_record["hostKind"] = Value::String("textbox".to_string());
|
||||
return Ok(range_record);
|
||||
}
|
||||
}
|
||||
PresentationElement::Shape(shape) => {
|
||||
if let Some((text, rich_text)) =
|
||||
shape.text.as_ref().zip(shape.rich_text.as_ref())
|
||||
&& let Some(range) =
|
||||
rich_text.ranges.iter().find(|range| range.range_id == range_id)
|
||||
{
|
||||
let mut range_record = text_range_to_proto(text, range);
|
||||
range_record["kind"] = Value::String("textRange".to_string());
|
||||
range_record["id"] = Value::String(id.to_string());
|
||||
range_record["slide"] = Value::from(slide_index + 1);
|
||||
range_record["slideIndex"] = Value::from(slide_index);
|
||||
range_record["hostAnchor"] =
|
||||
Value::String(format!("sh/{}", shape.element_id));
|
||||
range_record["hostKind"] = Value::String("textbox".to_string());
|
||||
return Ok(range_record);
|
||||
}
|
||||
}
|
||||
PresentationElement::Table(table) => {
|
||||
for (row_index, row) in table.rows.iter().enumerate() {
|
||||
for (column_index, cell) in row.iter().enumerate() {
|
||||
if let Some(range) = cell
|
||||
.rich_text
|
||||
.ranges
|
||||
.iter()
|
||||
.find(|range| range.range_id == range_id)
|
||||
{
|
||||
let mut range_record = text_range_to_proto(&cell.text, range);
|
||||
range_record["kind"] = Value::String("textRange".to_string());
|
||||
range_record["id"] = Value::String(id.to_string());
|
||||
range_record["slide"] = Value::from(slide_index + 1);
|
||||
range_record["slideIndex"] = Value::from(slide_index);
|
||||
range_record["hostAnchor"] = Value::String(format!(
|
||||
"tb/{}#cell/{row_index}/{column_index}",
|
||||
table.element_id
|
||||
));
|
||||
range_record["hostKind"] =
|
||||
Value::String("tableCell".to_string());
|
||||
return Ok(range_record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
PresentationElement::Connector(_)
|
||||
| PresentationElement::Image(_)
|
||||
| PresentationElement::Chart(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(thread_id) = id.strip_prefix("th/")
|
||||
&& let Some(thread) = document
|
||||
.comment_threads
|
||||
.iter()
|
||||
.find(|thread| thread.thread_id == thread_id)
|
||||
{
|
||||
let mut record = comment_thread_to_proto(thread);
|
||||
record["id"] = Value::String(id.to_string());
|
||||
return Ok(record);
|
||||
}
|
||||
|
||||
for layout in &document.layouts {
|
||||
let layout_id = format!("ly/{}", layout.layout_id);
|
||||
if id == layout_id {
|
||||
|
|
@ -608,4 +869,3 @@ fn resolve_anchor(
|
|||
message: format!("unknown resolve id `{id}`"),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct ThemeState {
|
||||
color_scheme: HashMap<String, String>,
|
||||
major_font: Option<String>,
|
||||
|
|
@ -27,13 +27,13 @@ impl ThemeState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum LayoutKind {
|
||||
Layout,
|
||||
Master,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct LayoutDocument {
|
||||
layout_id: String,
|
||||
name: String,
|
||||
|
|
@ -42,7 +42,7 @@ struct LayoutDocument {
|
|||
placeholders: Vec<PlaceholderDefinition>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PlaceholderDefinition {
|
||||
name: String,
|
||||
placeholder_type: String,
|
||||
|
|
@ -52,19 +52,21 @@ struct PlaceholderDefinition {
|
|||
frame: Rect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ResolvedPlaceholder {
|
||||
source_layout_id: String,
|
||||
definition: PlaceholderDefinition,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct NotesState {
|
||||
text: String,
|
||||
visible: bool,
|
||||
#[serde(default)]
|
||||
rich_text: RichTextState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct TextStyle {
|
||||
style_name: Option<String>,
|
||||
font_size: Option<u32>,
|
||||
|
|
@ -76,21 +78,21 @@ struct TextStyle {
|
|||
underline: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct NamedTextStyle {
|
||||
name: String,
|
||||
style: TextStyle,
|
||||
built_in: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct HyperlinkState {
|
||||
target: HyperlinkTarget,
|
||||
tooltip: Option<String>,
|
||||
highlight_click: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
enum HyperlinkTarget {
|
||||
Url(String),
|
||||
Slide(u32),
|
||||
|
|
@ -217,7 +219,7 @@ impl HyperlinkState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum TextAlignment {
|
||||
Left,
|
||||
|
|
@ -226,14 +228,188 @@ enum TextAlignment {
|
|||
Justify,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct RichTextState {
|
||||
#[serde(default)]
|
||||
ranges: Vec<TextRangeAnnotation>,
|
||||
#[serde(default)]
|
||||
layout: TextLayoutState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct TextRangeAnnotation {
|
||||
range_id: String,
|
||||
start_cp: usize,
|
||||
length: usize,
|
||||
style: TextStyle,
|
||||
hyperlink: Option<HyperlinkState>,
|
||||
spacing_before: Option<u32>,
|
||||
spacing_after: Option<u32>,
|
||||
line_spacing: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct TextLayoutState {
|
||||
insets: Option<TextInsets>,
|
||||
wrap: Option<TextWrapMode>,
|
||||
auto_fit: Option<TextAutoFitMode>,
|
||||
vertical_alignment: Option<TextVerticalAlignment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
struct TextInsets {
|
||||
left: u32,
|
||||
right: u32,
|
||||
top: u32,
|
||||
bottom: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum TextWrapMode {
|
||||
Square,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum TextAutoFitMode {
|
||||
None,
|
||||
ShrinkText,
|
||||
ResizeShapeToFitText,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum TextVerticalAlignment {
|
||||
Top,
|
||||
Middle,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CommentAuthorProfile {
|
||||
display_name: String,
|
||||
initials: String,
|
||||
email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CommentPosition {
|
||||
x: u32,
|
||||
y: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum CommentThreadStatus {
|
||||
Active,
|
||||
Resolved,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CommentMessage {
|
||||
message_id: String,
|
||||
author: CommentAuthorProfile,
|
||||
text: String,
|
||||
created_at: String,
|
||||
#[serde(default)]
|
||||
reactions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
enum CommentTarget {
|
||||
Slide {
|
||||
slide_id: String,
|
||||
},
|
||||
Element {
|
||||
slide_id: String,
|
||||
element_id: String,
|
||||
},
|
||||
TextRange {
|
||||
slide_id: String,
|
||||
element_id: String,
|
||||
start_cp: usize,
|
||||
length: usize,
|
||||
context: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CommentThread {
|
||||
thread_id: String,
|
||||
target: CommentTarget,
|
||||
position: Option<CommentPosition>,
|
||||
status: CommentThreadStatus,
|
||||
messages: Vec<CommentMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct TableBorder {
|
||||
color: String,
|
||||
width: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct TableBorders {
|
||||
outside: Option<TableBorder>,
|
||||
inside: Option<TableBorder>,
|
||||
top: Option<TableBorder>,
|
||||
bottom: Option<TableBorder>,
|
||||
left: Option<TableBorder>,
|
||||
right: Option<TableBorder>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct TableStyleOptions {
|
||||
header_row: bool,
|
||||
banded_rows: bool,
|
||||
banded_columns: bool,
|
||||
first_column: bool,
|
||||
last_column: bool,
|
||||
total_row: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct ChartMarkerStyle {
|
||||
symbol: Option<String>,
|
||||
size: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct ChartDataLabels {
|
||||
show_value: bool,
|
||||
show_category_name: bool,
|
||||
show_leader_lines: bool,
|
||||
position: Option<String>,
|
||||
text_style: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct ChartLegend {
|
||||
position: Option<String>,
|
||||
text_style: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct ChartAxisSpec {
|
||||
title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct ChartDataLabelOverride {
|
||||
idx: usize,
|
||||
text: Option<String>,
|
||||
position: Option<String>,
|
||||
text_style: TextStyle,
|
||||
fill: Option<String>,
|
||||
stroke: Option<StrokeStyle>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct PlaceholderRef {
|
||||
name: String,
|
||||
placeholder_type: String,
|
||||
index: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct TableMergeRegion {
|
||||
start_row: usize,
|
||||
end_row: usize,
|
||||
|
|
@ -241,15 +417,18 @@ struct TableMergeRegion {
|
|||
end_column: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct TableCellSpec {
|
||||
text: String,
|
||||
text_style: TextStyle,
|
||||
background_fill: Option<String>,
|
||||
alignment: Option<TextAlignment>,
|
||||
#[serde(default)]
|
||||
rich_text: RichTextState,
|
||||
borders: Option<TableBorders>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PresentationDocument {
|
||||
artifact_id: String,
|
||||
name: Option<String>,
|
||||
|
|
@ -259,9 +438,16 @@ struct PresentationDocument {
|
|||
layouts: Vec<LayoutDocument>,
|
||||
slides: Vec<PresentationSlide>,
|
||||
active_slide_index: Option<usize>,
|
||||
#[serde(default)]
|
||||
comment_self: Option<CommentAuthorProfile>,
|
||||
#[serde(default)]
|
||||
comment_threads: Vec<CommentThread>,
|
||||
next_slide_seq: u32,
|
||||
next_element_seq: u32,
|
||||
next_layout_seq: u32,
|
||||
next_text_range_seq: u32,
|
||||
next_comment_thread_seq: u32,
|
||||
next_comment_message_seq: u32,
|
||||
}
|
||||
|
||||
impl PresentationDocument {
|
||||
|
|
@ -280,9 +466,14 @@ impl PresentationDocument {
|
|||
layouts: Vec::new(),
|
||||
slides: Vec::new(),
|
||||
active_slide_index: None,
|
||||
comment_self: None,
|
||||
comment_threads: Vec::new(),
|
||||
next_slide_seq: 1,
|
||||
next_element_seq: 1,
|
||||
next_layout_seq: 1,
|
||||
next_text_range_seq: 1,
|
||||
next_comment_thread_seq: 1,
|
||||
next_comment_message_seq: 1,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -296,6 +487,7 @@ impl PresentationDocument {
|
|||
notes: NotesState {
|
||||
text: imported_slide.notes.clone().unwrap_or_default(),
|
||||
visible: true,
|
||||
rich_text: RichTextState::default(),
|
||||
},
|
||||
background_fill: None,
|
||||
layout_id: None,
|
||||
|
|
@ -316,6 +508,7 @@ impl PresentationDocument {
|
|||
fill: None,
|
||||
style: TextStyle::default(),
|
||||
hyperlink: None,
|
||||
rich_text: RichTextState::default(),
|
||||
placeholder: None,
|
||||
z_order: slide.elements.len(),
|
||||
}));
|
||||
|
|
@ -334,6 +527,7 @@ impl PresentationDocument {
|
|||
fill: None,
|
||||
style: TextStyle::default(),
|
||||
hyperlink: None,
|
||||
rich_text: RichTextState::default(),
|
||||
placeholder: None,
|
||||
z_order: slide.elements.len(),
|
||||
}));
|
||||
|
|
@ -360,6 +554,10 @@ impl PresentationDocument {
|
|||
text: imported_shape.text.clone(),
|
||||
text_style: TextStyle::default(),
|
||||
hyperlink: None,
|
||||
rich_text: imported_shape
|
||||
.text
|
||||
.as_ref()
|
||||
.map(|_| RichTextState::default()),
|
||||
placeholder: None,
|
||||
rotation_degrees: imported_shape.rotation,
|
||||
flip_horizontal: false,
|
||||
|
|
@ -390,6 +588,8 @@ impl PresentationDocument {
|
|||
text_style: TextStyle::default(),
|
||||
background_fill: None,
|
||||
alignment: None,
|
||||
rich_text: RichTextState::default(),
|
||||
borders: None,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
|
|
@ -407,6 +607,9 @@ impl PresentationDocument {
|
|||
.map(emu_to_points)
|
||||
.collect(),
|
||||
style: None,
|
||||
style_options: TableStyleOptions::default(),
|
||||
borders: None,
|
||||
right_to_left: false,
|
||||
merges: Vec::new(),
|
||||
z_order: slide.elements.len(),
|
||||
}));
|
||||
|
|
@ -434,6 +637,7 @@ impl PresentationDocument {
|
|||
notes: NotesState {
|
||||
text: notes.unwrap_or_default(),
|
||||
visible: true,
|
||||
rich_text: RichTextState::default(),
|
||||
},
|
||||
background_fill: normalized_fill,
|
||||
layout_id: None,
|
||||
|
|
@ -468,6 +672,24 @@ impl PresentationDocument {
|
|||
element_id
|
||||
}
|
||||
|
||||
fn next_text_range_id(&mut self) -> String {
|
||||
let range_id = format!("range_{}", self.next_text_range_seq);
|
||||
self.next_text_range_seq += 1;
|
||||
range_id
|
||||
}
|
||||
|
||||
fn next_comment_thread_id(&mut self) -> String {
|
||||
let thread_id = format!("thread_{}", self.next_comment_thread_seq);
|
||||
self.next_comment_thread_seq += 1;
|
||||
thread_id
|
||||
}
|
||||
|
||||
fn next_comment_message_id(&mut self) -> String {
|
||||
let message_id = format!("message_{}", self.next_comment_message_seq);
|
||||
self.next_comment_message_seq += 1;
|
||||
message_id
|
||||
}
|
||||
|
||||
fn total_element_count(&self) -> usize {
|
||||
self.slides.iter().map(|slide| slide.elements.len()).sum()
|
||||
}
|
||||
|
|
@ -717,7 +939,7 @@ impl PresentationDocument {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PresentationSlide {
|
||||
slide_id: String,
|
||||
notes: NotesState,
|
||||
|
|
@ -1190,7 +1412,7 @@ impl PresentationSlide {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
enum PresentationElement {
|
||||
Text(TextElement),
|
||||
Shape(ShapeElement),
|
||||
|
|
@ -1257,7 +1479,7 @@ impl PresentationElement {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct TextElement {
|
||||
element_id: String,
|
||||
text: String,
|
||||
|
|
@ -1265,11 +1487,13 @@ struct TextElement {
|
|||
fill: Option<String>,
|
||||
style: TextStyle,
|
||||
hyperlink: Option<HyperlinkState>,
|
||||
#[serde(default)]
|
||||
rich_text: RichTextState,
|
||||
placeholder: Option<PlaceholderRef>,
|
||||
z_order: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ShapeElement {
|
||||
element_id: String,
|
||||
geometry: ShapeGeometry,
|
||||
|
|
@ -1279,6 +1503,7 @@ struct ShapeElement {
|
|||
text: Option<String>,
|
||||
text_style: TextStyle,
|
||||
hyperlink: Option<HyperlinkState>,
|
||||
rich_text: Option<RichTextState>,
|
||||
placeholder: Option<PlaceholderRef>,
|
||||
rotation_degrees: Option<i32>,
|
||||
flip_horizontal: bool,
|
||||
|
|
@ -1286,7 +1511,7 @@ struct ShapeElement {
|
|||
z_order: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ConnectorElement {
|
||||
element_id: String,
|
||||
connector_type: ConnectorKind,
|
||||
|
|
@ -1301,7 +1526,7 @@ struct ConnectorElement {
|
|||
z_order: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ImageElement {
|
||||
pub(crate) element_id: String,
|
||||
pub(crate) frame: Rect,
|
||||
|
|
@ -1319,7 +1544,7 @@ pub(crate) struct ImageElement {
|
|||
pub(crate) z_order: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct TableElement {
|
||||
element_id: String,
|
||||
frame: Rect,
|
||||
|
|
@ -1327,11 +1552,14 @@ struct TableElement {
|
|||
column_widths: Vec<u32>,
|
||||
row_heights: Vec<u32>,
|
||||
style: Option<String>,
|
||||
style_options: TableStyleOptions,
|
||||
borders: Option<TableBorders>,
|
||||
right_to_left: bool,
|
||||
merges: Vec<TableMergeRegion>,
|
||||
z_order: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ChartElement {
|
||||
element_id: String,
|
||||
frame: Rect,
|
||||
|
|
@ -1339,10 +1567,18 @@ struct ChartElement {
|
|||
categories: Vec<String>,
|
||||
series: Vec<ChartSeriesSpec>,
|
||||
title: Option<String>,
|
||||
style_index: Option<u32>,
|
||||
has_legend: bool,
|
||||
legend: Option<ChartLegend>,
|
||||
x_axis: Option<ChartAxisSpec>,
|
||||
y_axis: Option<ChartAxisSpec>,
|
||||
data_labels: Option<ChartDataLabels>,
|
||||
chart_fill: Option<String>,
|
||||
plot_area_fill: Option<String>,
|
||||
z_order: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ImagePayload {
|
||||
pub(crate) bytes: Vec<u8>,
|
||||
pub(crate) format: String,
|
||||
|
|
@ -1350,13 +1586,20 @@ pub(crate) struct ImagePayload {
|
|||
pub(crate) height_px: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ChartSeriesSpec {
|
||||
name: String,
|
||||
values: Vec<f64>,
|
||||
categories: Option<Vec<String>>,
|
||||
x_values: Option<Vec<f64>>,
|
||||
fill: Option<String>,
|
||||
stroke: Option<StrokeStyle>,
|
||||
marker: Option<ChartMarkerStyle>,
|
||||
#[serde(default)]
|
||||
data_label_overrides: Vec<ChartDataLabelOverride>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum ShapeGeometry {
|
||||
Rectangle,
|
||||
RoundedRectangle,
|
||||
|
|
@ -1456,7 +1699,7 @@ impl ShapeGeometry {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum ChartTypeSpec {
|
||||
Bar,
|
||||
BarHorizontal,
|
||||
|
|
@ -1580,7 +1823,7 @@ impl LineStyle {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum ImageFitMode {
|
||||
Stretch,
|
||||
|
|
@ -1588,21 +1831,21 @@ pub(crate) enum ImageFitMode {
|
|||
Cover,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct StrokeStyle {
|
||||
color: String,
|
||||
width: u32,
|
||||
style: LineStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum ConnectorKind {
|
||||
Straight,
|
||||
Elbow,
|
||||
Curved,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum ConnectorArrowKind {
|
||||
None,
|
||||
Triangle,
|
||||
|
|
@ -1612,14 +1855,14 @@ enum ConnectorArrowKind {
|
|||
Open,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum ConnectorArrowScale {
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum LineStyle {
|
||||
Solid,
|
||||
Dashed,
|
||||
|
|
@ -1630,7 +1873,7 @@ enum LineStyle {
|
|||
LongDashDot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub(crate) struct Rect {
|
||||
pub(crate) left: u32,
|
||||
pub(crate) top: u32,
|
||||
|
|
|
|||
|
|
@ -169,6 +169,134 @@ fn parse_chart_type(
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_chart_marker(marker: Option<ChartMarkerArgs>) -> ChartMarkerStyle {
|
||||
marker
|
||||
.map(|marker| ChartMarkerStyle {
|
||||
symbol: marker.symbol,
|
||||
size: marker.size,
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn parse_chart_data_labels(
|
||||
document: &PresentationDocument,
|
||||
data_labels: Option<ChartDataLabelsArgs>,
|
||||
action: &str,
|
||||
) -> Result<Option<ChartDataLabels>, PresentationArtifactError> {
|
||||
data_labels
|
||||
.map(|data_labels| {
|
||||
Ok(ChartDataLabels {
|
||||
show_value: data_labels.show_value.unwrap_or(false),
|
||||
show_category_name: data_labels.show_category_name.unwrap_or(false),
|
||||
show_leader_lines: data_labels.show_leader_lines.unwrap_or(false),
|
||||
position: data_labels.position,
|
||||
text_style: normalize_text_style_with_document(
|
||||
document,
|
||||
&data_labels.text_style,
|
||||
action,
|
||||
)?,
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn parse_chart_series(
|
||||
document: &PresentationDocument,
|
||||
series: Vec<ChartSeriesArgs>,
|
||||
action: &str,
|
||||
) -> Result<Vec<ChartSeriesSpec>, PresentationArtifactError> {
|
||||
series
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
if entry.values.is_empty() {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!("series `{}` must contain at least one value", entry.name),
|
||||
});
|
||||
}
|
||||
let fill = entry
|
||||
.fill
|
||||
.as_deref()
|
||||
.map(|value| normalize_color_with_document(document, value, action, "series.fill"))
|
||||
.transpose()?;
|
||||
let stroke = entry
|
||||
.stroke
|
||||
.map(|stroke| {
|
||||
Ok(StrokeStyle {
|
||||
color: normalize_color_with_document(
|
||||
document,
|
||||
&stroke.color,
|
||||
action,
|
||||
"series.stroke.color",
|
||||
)?,
|
||||
width: stroke.width,
|
||||
style: stroke
|
||||
.style
|
||||
.as_deref()
|
||||
.map(|value| parse_line_style(value, action))
|
||||
.transpose()?
|
||||
.unwrap_or(LineStyle::Solid),
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
let data_label_overrides = entry
|
||||
.data_label_overrides
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|override_args| {
|
||||
Ok(ChartDataLabelOverride {
|
||||
idx: override_args.idx as usize,
|
||||
text: override_args.text,
|
||||
position: override_args.position,
|
||||
text_style: normalize_text_style_with_document(
|
||||
document,
|
||||
&override_args.text_style,
|
||||
action,
|
||||
)?,
|
||||
fill: override_args
|
||||
.fill
|
||||
.as_deref()
|
||||
.map(|value| {
|
||||
normalize_color_with_document(document, value, action, "fill")
|
||||
})
|
||||
.transpose()?,
|
||||
stroke: override_args
|
||||
.stroke
|
||||
.map(|stroke| {
|
||||
Ok(StrokeStyle {
|
||||
color: normalize_color_with_document(
|
||||
document,
|
||||
&stroke.color,
|
||||
action,
|
||||
"stroke.color",
|
||||
)?,
|
||||
width: stroke.width,
|
||||
style: stroke
|
||||
.style
|
||||
.as_deref()
|
||||
.map(|value| parse_line_style(value, action))
|
||||
.transpose()?
|
||||
.unwrap_or(LineStyle::Solid),
|
||||
})
|
||||
})
|
||||
.transpose()?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(ChartSeriesSpec {
|
||||
name: entry.name,
|
||||
values: entry.values,
|
||||
categories: entry.categories,
|
||||
x_values: entry.x_values,
|
||||
fill,
|
||||
stroke,
|
||||
marker: Some(parse_chart_marker(entry.marker)),
|
||||
data_label_overrides,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_stroke(
|
||||
document: &PresentationDocument,
|
||||
stroke: Option<StrokeArgs>,
|
||||
|
|
@ -299,6 +427,216 @@ fn normalize_text_style_with_document(
|
|||
})
|
||||
}
|
||||
|
||||
fn normalize_text_layout(
|
||||
layout: &TextLayoutArgs,
|
||||
action: &str,
|
||||
) -> Result<TextLayoutState, PresentationArtifactError> {
|
||||
let wrap = layout
|
||||
.wrap
|
||||
.as_deref()
|
||||
.map(|value| match value {
|
||||
"square" => Ok(TextWrapMode::Square),
|
||||
"none" => Ok(TextWrapMode::None),
|
||||
_ => Err(PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!("unsupported wrap `{value}`"),
|
||||
}),
|
||||
})
|
||||
.transpose()?;
|
||||
let auto_fit = layout
|
||||
.auto_fit
|
||||
.as_deref()
|
||||
.map(|value| match value {
|
||||
"none" => Ok(TextAutoFitMode::None),
|
||||
"shrinkText" | "shrink_text" => Ok(TextAutoFitMode::ShrinkText),
|
||||
"resizeShapeToFitText" | "resize_shape_to_fit_text" => {
|
||||
Ok(TextAutoFitMode::ResizeShapeToFitText)
|
||||
}
|
||||
_ => Err(PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!("unsupported auto_fit `{value}`"),
|
||||
}),
|
||||
})
|
||||
.transpose()?;
|
||||
let vertical_alignment = layout
|
||||
.vertical_alignment
|
||||
.as_deref()
|
||||
.map(|value| match value {
|
||||
"top" => Ok(TextVerticalAlignment::Top),
|
||||
"middle" | "center" => Ok(TextVerticalAlignment::Middle),
|
||||
"bottom" => Ok(TextVerticalAlignment::Bottom),
|
||||
_ => Err(PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!("unsupported vertical_alignment `{value}`"),
|
||||
}),
|
||||
})
|
||||
.transpose()?;
|
||||
Ok(TextLayoutState {
|
||||
insets: layout.insets.as_ref().map(|insets| TextInsets {
|
||||
left: insets.left,
|
||||
right: insets.right,
|
||||
top: insets.top,
|
||||
bottom: insets.bottom,
|
||||
}),
|
||||
wrap,
|
||||
auto_fit,
|
||||
vertical_alignment,
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_rich_text_input(
|
||||
document: &PresentationDocument,
|
||||
input: RichTextInput,
|
||||
action: &str,
|
||||
) -> Result<(String, RichTextState), PresentationArtifactError> {
|
||||
let mut text = String::new();
|
||||
let mut ranges = Vec::new();
|
||||
let paragraphs = match input {
|
||||
RichTextInput::Plain(value) => vec![RichParagraphInput::Plain(value)],
|
||||
RichTextInput::Paragraphs(paragraphs) => paragraphs,
|
||||
};
|
||||
for (paragraph_index, paragraph) in paragraphs.into_iter().enumerate() {
|
||||
if paragraph_index > 0 {
|
||||
text.push('\n');
|
||||
}
|
||||
let paragraph_start = text.chars().count();
|
||||
match paragraph {
|
||||
RichParagraphInput::Plain(value) => text.push_str(&value),
|
||||
RichParagraphInput::Runs(runs) => {
|
||||
for run in runs {
|
||||
match run {
|
||||
RichRunInput::Plain(value) => text.push_str(&value),
|
||||
RichRunInput::Styled(value) => {
|
||||
let start_cp = text.chars().count();
|
||||
text.push_str(&value.run);
|
||||
let length = value.run.chars().count();
|
||||
let style = normalize_text_style_with_document(
|
||||
document,
|
||||
&value.text_style,
|
||||
action,
|
||||
)?;
|
||||
let hyperlink = value
|
||||
.link
|
||||
.as_ref()
|
||||
.map(|link| parse_rich_text_link(link, action))
|
||||
.transpose()?;
|
||||
if length > 0
|
||||
&& (!text_style_is_empty(&style) || hyperlink.is_some())
|
||||
{
|
||||
ranges.push(TextRangeAnnotation {
|
||||
range_id: format!("inline_{paragraph_index}_{start_cp}"),
|
||||
start_cp,
|
||||
length,
|
||||
style,
|
||||
hyperlink,
|
||||
spacing_before: None,
|
||||
spacing_after: None,
|
||||
line_spacing: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let paragraph_len = text.chars().count() - paragraph_start;
|
||||
if paragraph_len == 0 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
text,
|
||||
RichTextState {
|
||||
ranges,
|
||||
layout: TextLayoutState::default(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_rich_text_link(
|
||||
link: &RichTextLinkInput,
|
||||
action: &str,
|
||||
) -> Result<HyperlinkState, PresentationArtifactError> {
|
||||
let uri = link
|
||||
.uri
|
||||
.as_ref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: "`link.uri` is required".to_string(),
|
||||
})?;
|
||||
let target = if link.is_external.unwrap_or(true) {
|
||||
HyperlinkTarget::Url(uri.clone())
|
||||
} else {
|
||||
HyperlinkTarget::File(uri.clone())
|
||||
};
|
||||
Ok(HyperlinkState {
|
||||
target,
|
||||
tooltip: None,
|
||||
highlight_click: true,
|
||||
})
|
||||
}
|
||||
|
||||
fn text_style_is_empty(style: &TextStyle) -> bool {
|
||||
style.style_name.is_none()
|
||||
&& style.font_size.is_none()
|
||||
&& style.font_family.is_none()
|
||||
&& style.color.is_none()
|
||||
&& style.alignment.is_none()
|
||||
&& !style.bold
|
||||
&& !style.italic
|
||||
&& !style.underline
|
||||
}
|
||||
|
||||
fn resolve_text_range_selector(
|
||||
text: &str,
|
||||
query: Option<&str>,
|
||||
occurrence: Option<usize>,
|
||||
start_cp: Option<usize>,
|
||||
length: Option<usize>,
|
||||
action: &str,
|
||||
) -> Result<(usize, usize, Option<String>), PresentationArtifactError> {
|
||||
if let Some(query) = query {
|
||||
let occurrence = occurrence.unwrap_or(0);
|
||||
let haystack = text.chars().collect::<Vec<_>>();
|
||||
let needle = query.chars().collect::<Vec<_>>();
|
||||
if needle.is_empty() {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: "`query` must not be empty".to_string(),
|
||||
});
|
||||
}
|
||||
let mut matches = Vec::new();
|
||||
for start in 0..=haystack.len().saturating_sub(needle.len()) {
|
||||
if haystack[start..start + needle.len()] == needle[..] {
|
||||
matches.push(start);
|
||||
}
|
||||
}
|
||||
let Some(found) = matches.get(occurrence).copied() else {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!("query `{query}` occurrence {occurrence} was not found"),
|
||||
});
|
||||
};
|
||||
return Ok((found, needle.len(), Some(query.to_string())));
|
||||
}
|
||||
let start_cp = start_cp.ok_or_else(|| PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: "provide either `query` or `start_cp`".to_string(),
|
||||
})?;
|
||||
let length = length.ok_or_else(|| PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: "`length` is required with `start_cp`".to_string(),
|
||||
})?;
|
||||
if start_cp + length > text.chars().count() {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: "text range is out of bounds".to_string(),
|
||||
});
|
||||
}
|
||||
Ok((start_cp, length, None))
|
||||
}
|
||||
|
||||
fn normalize_text_style_with_palette(
|
||||
theme: Option<&ThemeState>,
|
||||
styling: &TextStylingArgs,
|
||||
|
|
@ -423,12 +761,82 @@ fn coerce_table_rows(
|
|||
text_style: TextStyle::default(),
|
||||
background_fill: None,
|
||||
alignment: None,
|
||||
rich_text: RichTextState::default(),
|
||||
borders: None,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn parse_table_border(
|
||||
document: &PresentationDocument,
|
||||
border: &TableBorderArgs,
|
||||
action: &str,
|
||||
field: &str,
|
||||
) -> Result<TableBorder, PresentationArtifactError> {
|
||||
Ok(TableBorder {
|
||||
color: normalize_color_with_document(document, &border.color, action, field)?,
|
||||
width: border.width,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_table_borders(
|
||||
document: &PresentationDocument,
|
||||
borders: Option<TableBordersArgs>,
|
||||
action: &str,
|
||||
) -> Result<Option<TableBorders>, PresentationArtifactError> {
|
||||
borders
|
||||
.map(|borders| {
|
||||
Ok(TableBorders {
|
||||
outside: borders
|
||||
.outside
|
||||
.as_ref()
|
||||
.map(|border| parse_table_border(document, border, action, "borders.outside"))
|
||||
.transpose()?,
|
||||
inside: borders
|
||||
.inside
|
||||
.as_ref()
|
||||
.map(|border| parse_table_border(document, border, action, "borders.inside"))
|
||||
.transpose()?,
|
||||
top: borders
|
||||
.top
|
||||
.as_ref()
|
||||
.map(|border| parse_table_border(document, border, action, "borders.top"))
|
||||
.transpose()?,
|
||||
bottom: borders
|
||||
.bottom
|
||||
.as_ref()
|
||||
.map(|border| parse_table_border(document, border, action, "borders.bottom"))
|
||||
.transpose()?,
|
||||
left: borders
|
||||
.left
|
||||
.as_ref()
|
||||
.map(|border| parse_table_border(document, border, action, "borders.left"))
|
||||
.transpose()?,
|
||||
right: borders
|
||||
.right
|
||||
.as_ref()
|
||||
.map(|border| parse_table_border(document, border, action, "borders.right"))
|
||||
.transpose()?,
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn parse_table_style_options(style_options: Option<TableStyleOptionsArgs>) -> TableStyleOptions {
|
||||
style_options
|
||||
.map(|style_options| TableStyleOptions {
|
||||
header_row: style_options.header_row.unwrap_or(false),
|
||||
banded_rows: style_options.banded_rows.unwrap_or(false),
|
||||
banded_columns: style_options.banded_columns.unwrap_or(false),
|
||||
first_column: style_options.first_column.unwrap_or(false),
|
||||
last_column: style_options.last_column.unwrap_or(false),
|
||||
total_row: style_options.total_row.unwrap_or(false),
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn normalize_table_dimensions(
|
||||
rows: &[Vec<TableCellSpec>],
|
||||
frame: Rect,
|
||||
|
|
@ -685,6 +1093,7 @@ fn materialize_placeholder_element(
|
|||
fill: None,
|
||||
style: TextStyle::default(),
|
||||
hyperlink: None,
|
||||
rich_text: RichTextState::default(),
|
||||
placeholder: placeholder_ref,
|
||||
z_order,
|
||||
})
|
||||
|
|
@ -698,6 +1107,7 @@ fn materialize_placeholder_element(
|
|||
text: placeholder.text,
|
||||
text_style: TextStyle::default(),
|
||||
hyperlink: None,
|
||||
rich_text: None,
|
||||
placeholder: placeholder_ref,
|
||||
rotation_degrees: None,
|
||||
flip_horizontal: false,
|
||||
|
|
@ -861,4 +1271,3 @@ fn slide_placeholder_list(
|
|||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,21 @@
|
|||
const CODEX_METADATA_ENTRY: &str = "ppt/codex-document.json";
|
||||
|
||||
fn import_codex_metadata_document(path: &Path) -> Result<Option<PresentationDocument>, String> {
|
||||
let file = std::fs::File::open(path).map_err(|error| error.to_string())?;
|
||||
let mut archive = ZipArchive::new(file).map_err(|error| error.to_string())?;
|
||||
let mut entry = match archive.by_name(CODEX_METADATA_ENTRY) {
|
||||
Ok(entry) => entry,
|
||||
Err(zip::result::ZipError::FileNotFound) => return Ok(None),
|
||||
Err(error) => return Err(error.to_string()),
|
||||
};
|
||||
let mut bytes = Vec::new();
|
||||
entry.read_to_end(&mut bytes)
|
||||
.map_err(|error| error.to_string())?;
|
||||
serde_json::from_slice(&bytes)
|
||||
.map(Some)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn build_pptx_bytes(document: &PresentationDocument, action: &str) -> Result<Vec<u8>, String> {
|
||||
let bytes = document
|
||||
.to_ppt_rs()
|
||||
|
|
@ -211,6 +229,9 @@ fn patch_pptx_package(
|
|||
continue;
|
||||
}
|
||||
let name = file.name().to_string();
|
||||
if name == CODEX_METADATA_ENTRY {
|
||||
continue;
|
||||
}
|
||||
let options = file.options();
|
||||
let mut bytes = Vec::new();
|
||||
file.read_to_end(&mut bytes)
|
||||
|
|
@ -282,6 +303,15 @@ fn patch_pptx_package(
|
|||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
|
||||
writer
|
||||
.start_file(CODEX_METADATA_ENTRY, SimpleFileOptions::default())
|
||||
.map_err(|error| error.to_string())?;
|
||||
writer
|
||||
.write_all(
|
||||
&serde_json::to_vec(document).map_err(|error| error.to_string())?,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
|
||||
writer
|
||||
.finish()
|
||||
.map_err(|error| error.to_string())
|
||||
|
|
@ -919,4 +949,3 @@ fn normalize_preview_quality(
|
|||
}
|
||||
Ok(quality)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ fn document_to_proto(
|
|||
"masters": document.layouts.iter().filter(|layout| layout.kind == LayoutKind::Master).map(|layout| layout.layout_id.clone()).collect::<Vec<_>>(),
|
||||
"layouts": layouts,
|
||||
"slides": slides,
|
||||
"commentAuthor": document.comment_self.as_ref().map(comment_author_to_proto),
|
||||
"commentThreads": document
|
||||
.comment_threads
|
||||
.iter()
|
||||
.map(comment_thread_to_proto)
|
||||
.collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +101,7 @@ fn slide_to_proto(slide: &PresentationSlide, slide_index: usize) -> Value {
|
|||
"textPreview": slide.notes.text.replace('\n', " | "),
|
||||
"textChars": slide.notes.text.chars().count(),
|
||||
"textLines": slide.notes.text.lines().count(),
|
||||
"richText": rich_text_to_proto(&slide.notes.text, &slide.notes.rich_text),
|
||||
}),
|
||||
"elements": slide.elements.iter().map(element_to_proto).collect::<Vec<_>>(),
|
||||
})
|
||||
|
|
@ -114,6 +121,7 @@ fn element_to_proto(element: &PresentationElement) -> Value {
|
|||
"textLines": text.text.lines().count(),
|
||||
"fill": text.fill,
|
||||
"style": text_style_to_proto(&text.style),
|
||||
"richText": rich_text_to_proto(&text.text, &text.rich_text),
|
||||
"zOrder": text.z_order,
|
||||
});
|
||||
if let Some(placeholder) = &text.placeholder {
|
||||
|
|
@ -135,6 +143,12 @@ fn element_to_proto(element: &PresentationElement) -> Value {
|
|||
"stroke": shape.stroke.as_ref().map(stroke_to_proto),
|
||||
"text": shape.text,
|
||||
"textStyle": text_style_to_proto(&shape.text_style),
|
||||
"richText": shape
|
||||
.text
|
||||
.as_ref()
|
||||
.zip(shape.rich_text.as_ref())
|
||||
.map(|(text, rich_text)| rich_text_to_proto(text, rich_text))
|
||||
.unwrap_or(Value::Null),
|
||||
"rotation": shape.rotation_degrees,
|
||||
"flipHorizontal": shape.flip_horizontal,
|
||||
"flipVertical": shape.flip_vertical,
|
||||
|
|
@ -215,6 +229,9 @@ fn element_to_proto(element: &PresentationElement) -> Value {
|
|||
"columnWidths": table.column_widths,
|
||||
"rowHeights": table.row_heights,
|
||||
"style": table.style,
|
||||
"styleOptions": table_style_options_to_proto(&table.style_options),
|
||||
"borders": table.borders.as_ref().map(table_borders_to_proto),
|
||||
"rightToLeft": table.right_to_left,
|
||||
"merges": table.merges.iter().map(|merge| serde_json::json!({
|
||||
"startRow": merge.start_row,
|
||||
"endRow": merge.end_row,
|
||||
|
|
@ -231,9 +248,27 @@ fn element_to_proto(element: &PresentationElement) -> Value {
|
|||
"chartType": format!("{:?}", chart.chart_type),
|
||||
"title": chart.title,
|
||||
"categories": chart.categories,
|
||||
"styleIndex": chart.style_index,
|
||||
"hasLegend": chart.has_legend,
|
||||
"legend": chart.legend.as_ref().map(chart_legend_to_proto),
|
||||
"xAxis": chart.x_axis.as_ref().map(chart_axis_to_proto),
|
||||
"yAxis": chart.y_axis.as_ref().map(chart_axis_to_proto),
|
||||
"dataLabels": chart.data_labels.as_ref().map(chart_data_labels_to_proto),
|
||||
"chartFill": chart.chart_fill,
|
||||
"plotAreaFill": chart.plot_area_fill,
|
||||
"series": chart.series.iter().map(|series| serde_json::json!({
|
||||
"name": series.name,
|
||||
"values": series.values,
|
||||
"categories": series.categories,
|
||||
"xValues": series.x_values,
|
||||
"fill": series.fill,
|
||||
"stroke": series.stroke.as_ref().map(stroke_to_proto),
|
||||
"marker": series.marker.as_ref().map(chart_marker_to_proto),
|
||||
"dataLabelOverrides": series
|
||||
.data_label_overrides
|
||||
.iter()
|
||||
.map(chart_data_label_override_to_proto)
|
||||
.collect::<Vec<_>>(),
|
||||
})).collect::<Vec<_>>(),
|
||||
"zOrder": chart.z_order,
|
||||
}),
|
||||
|
|
@ -272,6 +307,72 @@ fn text_style_to_proto(style: &TextStyle) -> Value {
|
|||
})
|
||||
}
|
||||
|
||||
fn rich_text_to_proto(text: &str, rich_text: &RichTextState) -> Value {
|
||||
serde_json::json!({
|
||||
"layout": text_layout_to_proto(&rich_text.layout),
|
||||
"ranges": rich_text
|
||||
.ranges
|
||||
.iter()
|
||||
.map(|range| text_range_to_proto(text, range))
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
|
||||
fn text_range_to_proto(text: &str, range: &TextRangeAnnotation) -> Value {
|
||||
serde_json::json!({
|
||||
"rangeId": range.range_id,
|
||||
"anchor": format!("tr/{}", range.range_id),
|
||||
"startCp": range.start_cp,
|
||||
"length": range.length,
|
||||
"text": text_slice_by_codepoint_range(text, range.start_cp, range.length),
|
||||
"style": text_style_to_proto(&range.style),
|
||||
"hyperlink": range.hyperlink.as_ref().map(HyperlinkState::to_json),
|
||||
"spacingBefore": range.spacing_before,
|
||||
"spacingAfter": range.spacing_after,
|
||||
"lineSpacing": range.line_spacing,
|
||||
})
|
||||
}
|
||||
|
||||
fn text_layout_to_proto(layout: &TextLayoutState) -> Value {
|
||||
serde_json::json!({
|
||||
"insets": layout.insets.map(|insets| serde_json::json!({
|
||||
"left": insets.left,
|
||||
"right": insets.right,
|
||||
"top": insets.top,
|
||||
"bottom": insets.bottom,
|
||||
"unit": "points",
|
||||
})),
|
||||
"wrap": layout.wrap.map(text_wrap_mode_to_proto),
|
||||
"autoFit": layout.auto_fit.map(text_auto_fit_mode_to_proto),
|
||||
"verticalAlignment": layout
|
||||
.vertical_alignment
|
||||
.map(text_vertical_alignment_to_proto),
|
||||
})
|
||||
}
|
||||
|
||||
fn text_wrap_mode_to_proto(mode: TextWrapMode) -> &'static str {
|
||||
match mode {
|
||||
TextWrapMode::Square => "square",
|
||||
TextWrapMode::None => "none",
|
||||
}
|
||||
}
|
||||
|
||||
fn text_auto_fit_mode_to_proto(mode: TextAutoFitMode) -> &'static str {
|
||||
match mode {
|
||||
TextAutoFitMode::None => "none",
|
||||
TextAutoFitMode::ShrinkText => "shrinkText",
|
||||
TextAutoFitMode::ResizeShapeToFitText => "resizeShapeToFitText",
|
||||
}
|
||||
}
|
||||
|
||||
fn text_vertical_alignment_to_proto(alignment: TextVerticalAlignment) -> &'static str {
|
||||
match alignment {
|
||||
TextVerticalAlignment::Top => "top",
|
||||
TextVerticalAlignment::Middle => "middle",
|
||||
TextVerticalAlignment::Bottom => "bottom",
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_ref_to_proto(placeholder: &PlaceholderRef) -> Value {
|
||||
serde_json::json!({
|
||||
"name": placeholder.name,
|
||||
|
|
@ -293,11 +394,169 @@ fn table_cell_to_proto(cell: &TableCellSpec) -> Value {
|
|||
serde_json::json!({
|
||||
"text": cell.text,
|
||||
"textStyle": text_style_to_proto(&cell.text_style),
|
||||
"richText": rich_text_to_proto(&cell.text, &cell.rich_text),
|
||||
"backgroundFill": cell.background_fill,
|
||||
"alignment": cell.alignment,
|
||||
"borders": cell.borders.as_ref().map(table_borders_to_proto),
|
||||
})
|
||||
}
|
||||
|
||||
fn table_style_options_to_proto(style_options: &TableStyleOptions) -> Value {
|
||||
serde_json::json!({
|
||||
"headerRow": style_options.header_row,
|
||||
"bandedRows": style_options.banded_rows,
|
||||
"bandedColumns": style_options.banded_columns,
|
||||
"firstColumn": style_options.first_column,
|
||||
"lastColumn": style_options.last_column,
|
||||
"totalRow": style_options.total_row,
|
||||
})
|
||||
}
|
||||
|
||||
fn table_borders_to_proto(borders: &TableBorders) -> Value {
|
||||
serde_json::json!({
|
||||
"outside": borders.outside.as_ref().map(table_border_to_proto),
|
||||
"inside": borders.inside.as_ref().map(table_border_to_proto),
|
||||
"top": borders.top.as_ref().map(table_border_to_proto),
|
||||
"bottom": borders.bottom.as_ref().map(table_border_to_proto),
|
||||
"left": borders.left.as_ref().map(table_border_to_proto),
|
||||
"right": borders.right.as_ref().map(table_border_to_proto),
|
||||
})
|
||||
}
|
||||
|
||||
fn table_border_to_proto(border: &TableBorder) -> Value {
|
||||
serde_json::json!({
|
||||
"color": border.color,
|
||||
"width": border.width,
|
||||
"unit": "points",
|
||||
})
|
||||
}
|
||||
|
||||
fn chart_marker_to_proto(marker: &ChartMarkerStyle) -> Value {
|
||||
serde_json::json!({
|
||||
"symbol": marker.symbol,
|
||||
"size": marker.size,
|
||||
})
|
||||
}
|
||||
|
||||
fn chart_data_labels_to_proto(data_labels: &ChartDataLabels) -> Value {
|
||||
serde_json::json!({
|
||||
"showValue": data_labels.show_value,
|
||||
"showCategoryName": data_labels.show_category_name,
|
||||
"showLeaderLines": data_labels.show_leader_lines,
|
||||
"position": data_labels.position,
|
||||
"textStyle": text_style_to_proto(&data_labels.text_style),
|
||||
})
|
||||
}
|
||||
|
||||
fn chart_legend_to_proto(legend: &ChartLegend) -> Value {
|
||||
serde_json::json!({
|
||||
"position": legend.position,
|
||||
"textStyle": text_style_to_proto(&legend.text_style),
|
||||
})
|
||||
}
|
||||
|
||||
fn chart_axis_to_proto(axis: &ChartAxisSpec) -> Value {
|
||||
serde_json::json!({
|
||||
"title": axis.title,
|
||||
})
|
||||
}
|
||||
|
||||
fn chart_data_label_override_to_proto(override_spec: &ChartDataLabelOverride) -> Value {
|
||||
serde_json::json!({
|
||||
"idx": override_spec.idx,
|
||||
"text": override_spec.text,
|
||||
"position": override_spec.position,
|
||||
"textStyle": text_style_to_proto(&override_spec.text_style),
|
||||
"fill": override_spec.fill,
|
||||
"stroke": override_spec.stroke.as_ref().map(stroke_to_proto),
|
||||
})
|
||||
}
|
||||
|
||||
fn comment_author_to_proto(author: &CommentAuthorProfile) -> Value {
|
||||
serde_json::json!({
|
||||
"displayName": author.display_name,
|
||||
"initials": author.initials,
|
||||
"email": author.email,
|
||||
})
|
||||
}
|
||||
|
||||
fn comment_thread_to_proto(thread: &CommentThread) -> Value {
|
||||
serde_json::json!({
|
||||
"kind": "comment",
|
||||
"threadId": thread.thread_id,
|
||||
"anchor": format!("th/{}", thread.thread_id),
|
||||
"target": comment_target_to_proto(&thread.target),
|
||||
"position": thread.position.as_ref().map(comment_position_to_proto),
|
||||
"status": comment_status_to_proto(thread.status),
|
||||
"messages": thread.messages.iter().map(comment_message_to_proto).collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
|
||||
fn comment_target_to_proto(target: &CommentTarget) -> Value {
|
||||
match target {
|
||||
CommentTarget::Slide { slide_id } => serde_json::json!({
|
||||
"type": "slide",
|
||||
"slideId": slide_id,
|
||||
"slideAnchor": format!("sl/{slide_id}"),
|
||||
}),
|
||||
CommentTarget::Element {
|
||||
slide_id,
|
||||
element_id,
|
||||
} => serde_json::json!({
|
||||
"type": "element",
|
||||
"slideId": slide_id,
|
||||
"slideAnchor": format!("sl/{slide_id}"),
|
||||
"elementId": element_id,
|
||||
"elementAnchor": format!("sh/{element_id}"),
|
||||
}),
|
||||
CommentTarget::TextRange {
|
||||
slide_id,
|
||||
element_id,
|
||||
start_cp,
|
||||
length,
|
||||
context,
|
||||
} => serde_json::json!({
|
||||
"type": "textRange",
|
||||
"slideId": slide_id,
|
||||
"slideAnchor": format!("sl/{slide_id}"),
|
||||
"elementId": element_id,
|
||||
"elementAnchor": format!("sh/{element_id}"),
|
||||
"startCp": start_cp,
|
||||
"length": length,
|
||||
"context": context,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn comment_position_to_proto(position: &CommentPosition) -> Value {
|
||||
serde_json::json!({
|
||||
"x": position.x,
|
||||
"y": position.y,
|
||||
"unit": "points",
|
||||
})
|
||||
}
|
||||
|
||||
fn comment_message_to_proto(message: &CommentMessage) -> Value {
|
||||
serde_json::json!({
|
||||
"messageId": message.message_id,
|
||||
"author": comment_author_to_proto(&message.author),
|
||||
"text": message.text,
|
||||
"createdAt": message.created_at,
|
||||
"reactions": message.reactions,
|
||||
})
|
||||
}
|
||||
|
||||
fn comment_status_to_proto(status: CommentThreadStatus) -> &'static str {
|
||||
match status {
|
||||
CommentThreadStatus::Active => "active",
|
||||
CommentThreadStatus::Resolved => "resolved",
|
||||
}
|
||||
}
|
||||
|
||||
fn text_slice_by_codepoint_range(text: &str, start_cp: usize, length: usize) -> String {
|
||||
text.chars().skip(start_cp).take(length).collect()
|
||||
}
|
||||
|
||||
fn build_table_cell(
|
||||
cell: TableCellSpec,
|
||||
merges: &[TableMergeRegion],
|
||||
|
|
@ -353,4 +612,3 @@ fn build_table_cell(
|
|||
}
|
||||
table_cell
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ fn slide_list(document: &PresentationDocument) -> Vec<SlideListEntry> {
|
|||
slide_id: slide.slide_id.clone(),
|
||||
index,
|
||||
is_active: document.active_slide_index == Some(index),
|
||||
notes: (!slide.notes.text.is_empty()).then(|| slide.notes.text.clone()),
|
||||
notes: (slide.notes.visible && !slide.notes.text.is_empty())
|
||||
.then(|| slide.notes.text.clone()),
|
||||
notes_visible: slide.notes.visible,
|
||||
background_fill: slide.background_fill.clone(),
|
||||
layout_id: slide.layout_id.clone(),
|
||||
|
|
|
|||
|
|
@ -1954,6 +1954,15 @@ fn inspect_supports_filters_target_windows_and_shape_text_metadata()
|
|||
"italic": false,
|
||||
"underline": false
|
||||
},
|
||||
"richText": {
|
||||
"layout": {
|
||||
"insets": serde_json::Value::Null,
|
||||
"wrap": serde_json::Value::Null,
|
||||
"autoFit": serde_json::Value::Null,
|
||||
"verticalAlignment": serde_json::Value::Null
|
||||
},
|
||||
"ranges": []
|
||||
},
|
||||
"rotation": serde_json::Value::Null,
|
||||
"flipHorizontal": false,
|
||||
"flipVertical": false,
|
||||
|
|
@ -2816,6 +2825,441 @@ fn manager_supports_table_cell_updates_and_merges() -> Result<(), Box<dyn std::e
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rich_text_comments_tables_and_charts_roundtrip_through_metadata()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let created = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({ "name": "Parity Roundtrip" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let artifact_id = created.artifact_id;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let text_added = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_text_shape".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"text": "Placeholder",
|
||||
"position": { "left": 32, "top": 28, "width": 280, "height": 72 }
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let text_id = text_added
|
||||
.artifact_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.slides.first())
|
||||
.and_then(|slide| slide.element_ids.first())
|
||||
.cloned()
|
||||
.expect("text id");
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "set_rich_text".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": text_id,
|
||||
"text": [[
|
||||
{
|
||||
"run": "Quarterly ",
|
||||
"text_style": { "bold": true, "color": "#114488" }
|
||||
},
|
||||
"update pipeline"
|
||||
]],
|
||||
"text_layout": {
|
||||
"wrap": "square",
|
||||
"auto_fit": "shrinkText",
|
||||
"vertical_alignment": "middle",
|
||||
"insets": { "left": 6, "right": 6, "top": 4, "bottom": 4 }
|
||||
}
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "format_text_range".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": text_id,
|
||||
"query": "update",
|
||||
"styling": { "italic": true },
|
||||
"link": { "uri": "https://example.com/update", "is_external": true }
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "set_comment_author".to_string(),
|
||||
args: serde_json::json!({
|
||||
"display_name": "Jamie Fox",
|
||||
"initials": "JF",
|
||||
"email": "jamie@example.com"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_comment_thread".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"element_id": text_id,
|
||||
"query": "Quarterly",
|
||||
"text": "Tighten this headline",
|
||||
"position": { "x": 240, "y": 44 }
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_comment_reply".to_string(),
|
||||
args: serde_json::json!({
|
||||
"thread_id": "thread_1",
|
||||
"text": "Applied to the slide draft."
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "toggle_comment_reaction".to_string(),
|
||||
args: serde_json::json!({
|
||||
"thread_id": "thread_1",
|
||||
"emoji": "eyes"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "resolve_comment_thread".to_string(),
|
||||
args: serde_json::json!({ "thread_id": "thread_1" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "reopen_comment_thread".to_string(),
|
||||
args: serde_json::json!({ "thread_id": "thread_1" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let table_added = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_table".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"position": { "left": 32, "top": 124, "width": 320, "height": 120 },
|
||||
"rows": [["Metric", "Value"], ["Status", "Beta"]],
|
||||
"style": "TableStyleMedium2",
|
||||
"style_options": {
|
||||
"header_row": true,
|
||||
"banded_rows": true,
|
||||
"first_column": true
|
||||
},
|
||||
"borders": {
|
||||
"outside": { "color": "#222222", "width": 2 }
|
||||
},
|
||||
"right_to_left": true
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let table_id = table_added
|
||||
.artifact_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.slides.first())
|
||||
.and_then(|slide| slide.element_ids.last())
|
||||
.cloned()
|
||||
.expect("table id");
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "update_table_style".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": table_id,
|
||||
"style_options": { "last_column": true, "total_row": true },
|
||||
"borders": {
|
||||
"inside": { "color": "#999999", "width": 1 }
|
||||
}
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "style_table_block".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": table_id,
|
||||
"row": 1,
|
||||
"column": 0,
|
||||
"row_count": 1,
|
||||
"column_count": 2,
|
||||
"background_fill": "#FFF2CC",
|
||||
"alignment": "center",
|
||||
"styling": { "bold": true }
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "format_text_range".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": table_id,
|
||||
"row": 1,
|
||||
"column": 1,
|
||||
"query": "Beta",
|
||||
"styling": { "italic": true, "color": "#AA0000" }
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let chart_added = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_chart".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"position": { "left": 372, "top": 120, "width": 280, "height": 210 },
|
||||
"chart_type": "bar",
|
||||
"categories": ["Q1", "Q2"],
|
||||
"series": [{
|
||||
"name": "Revenue",
|
||||
"values": [10.0, 12.0],
|
||||
"fill": "#4472C4",
|
||||
"stroke": { "color": "#1F4E79", "width": 2 },
|
||||
"marker": { "symbol": "circle", "size": 7 },
|
||||
"data_label_overrides": [{
|
||||
"idx": 1,
|
||||
"text": "12M",
|
||||
"position": "outsideEnd",
|
||||
"fill": "#FFFFFF"
|
||||
}]
|
||||
}],
|
||||
"title": "Revenue",
|
||||
"style_index": 7,
|
||||
"has_legend": true,
|
||||
"legend_position": "right",
|
||||
"legend_text_style": { "italic": true },
|
||||
"x_axis_title": "Quarter",
|
||||
"y_axis_title": "USD",
|
||||
"data_labels": {
|
||||
"show_value": true,
|
||||
"position": "outsideEnd",
|
||||
"text_style": { "bold": true }
|
||||
},
|
||||
"chart_fill": "#F8F8F8",
|
||||
"plot_area_fill": "#FFFFFF"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let chart_id = chart_added
|
||||
.artifact_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.slides.first())
|
||||
.and_then(|slide| slide.element_ids.last())
|
||||
.cloned()
|
||||
.expect("chart id");
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "update_chart".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": chart_id,
|
||||
"title": "Revenue outlook",
|
||||
"style_index": 12,
|
||||
"legend_position": "bottom",
|
||||
"y_axis_title": "USD (millions)"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_chart_series".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": chart_id,
|
||||
"name": "Target",
|
||||
"values": [11.0, 13.0],
|
||||
"fill": "#70AD47",
|
||||
"marker": { "symbol": "diamond", "size": 6 }
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let export_path = temp_dir.path().join("parity-roundtrip.pptx");
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id),
|
||||
action: "export_pptx".to_string(),
|
||||
args: serde_json::json!({ "path": export_path }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let metadata = zip_entry_text(
|
||||
&temp_dir.path().join("parity-roundtrip.pptx"),
|
||||
"ppt/codex-document.json",
|
||||
)?;
|
||||
assert!(metadata.contains("\"thread_id\":\"thread_1\""));
|
||||
assert!(metadata.contains("\"style_index\":12"));
|
||||
assert!(metadata.contains("\"right_to_left\":true"));
|
||||
|
||||
let imported = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "import_pptx".to_string(),
|
||||
args: serde_json::json!({ "path": "parity-roundtrip.pptx" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let imported_artifact_id = imported.artifact_id;
|
||||
|
||||
let proto = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(imported_artifact_id.clone()),
|
||||
action: "to_proto".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let proto = proto.proto_json.expect("proto");
|
||||
let comments = proto["commentThreads"].as_array().expect("comments");
|
||||
assert_eq!(comments.len(), 1);
|
||||
assert_eq!(comments[0]["status"], "active");
|
||||
assert_eq!(comments[0]["messages"].as_array().map(Vec::len), Some(2));
|
||||
assert_eq!(
|
||||
comments[0]["messages"][1]["reactions"],
|
||||
serde_json::json!(["eyes"])
|
||||
);
|
||||
|
||||
let elements = proto["slides"][0]["elements"].as_array().expect("elements");
|
||||
let text_record = elements
|
||||
.iter()
|
||||
.find(|element| element["elementId"] == text_id)
|
||||
.expect("text record");
|
||||
assert_eq!(text_record["richText"]["layout"]["wrap"], "square");
|
||||
assert_eq!(text_record["richText"]["layout"]["autoFit"], "shrinkText");
|
||||
assert_eq!(
|
||||
text_record["richText"]["layout"]["verticalAlignment"],
|
||||
"middle"
|
||||
);
|
||||
assert_eq!(
|
||||
text_record["richText"]["ranges"].as_array().map(Vec::len),
|
||||
Some(2)
|
||||
);
|
||||
assert!(
|
||||
text_record["richText"]["ranges"]
|
||||
.as_array()
|
||||
.expect("text ranges")
|
||||
.iter()
|
||||
.any(|range| range["text"] == "update")
|
||||
);
|
||||
|
||||
let table_record = elements
|
||||
.iter()
|
||||
.find(|element| element["elementId"] == table_id)
|
||||
.expect("table record");
|
||||
assert_eq!(table_record["styleOptions"]["headerRow"], true);
|
||||
assert_eq!(table_record["styleOptions"]["lastColumn"], true);
|
||||
assert_eq!(table_record["rightToLeft"], true);
|
||||
assert_eq!(table_record["borders"]["outside"]["color"], "222222");
|
||||
assert_eq!(
|
||||
table_record["rows"][1][1]["richText"]["ranges"]
|
||||
.as_array()
|
||||
.map(Vec::len),
|
||||
Some(1)
|
||||
);
|
||||
|
||||
let chart_record = elements
|
||||
.iter()
|
||||
.find(|element| element["elementId"] == chart_id)
|
||||
.expect("chart record");
|
||||
assert_eq!(chart_record["styleIndex"], 12);
|
||||
assert_eq!(chart_record["legend"]["position"], "bottom");
|
||||
assert_eq!(chart_record["series"].as_array().map(Vec::len), Some(2));
|
||||
assert_eq!(chart_record["series"][1]["name"], "Target");
|
||||
|
||||
let inspect = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(imported_artifact_id.clone()),
|
||||
action: "inspect".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let inspect_ndjson = inspect.inspect_ndjson.expect("inspect");
|
||||
assert!(inspect_ndjson.contains("\"kind\":\"comment\""));
|
||||
assert!(inspect_ndjson.contains("\"kind\":\"textRange\""));
|
||||
assert!(inspect_ndjson.contains("\"chartType\":\"Bar\""));
|
||||
|
||||
let resolved_thread = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(imported_artifact_id.clone()),
|
||||
action: "resolve".to_string(),
|
||||
args: serde_json::json!({ "id": "th/thread_1" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(
|
||||
resolved_thread
|
||||
.resolved_record
|
||||
.as_ref()
|
||||
.and_then(|record| record.get("target"))
|
||||
.and_then(|target| target.get("type")),
|
||||
Some(&serde_json::json!("textRange"))
|
||||
);
|
||||
|
||||
let resolved_range = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(imported_artifact_id),
|
||||
action: "resolve".to_string(),
|
||||
args: serde_json::json!({ "id": "tr/range_1" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(
|
||||
resolved_range
|
||||
.resolved_record
|
||||
.as_ref()
|
||||
.and_then(|record| record.get("text")),
|
||||
Some(&serde_json::json!("update"))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_can_undo_and_redo_created_artifact() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
|
|
@ -2971,160 +3415,32 @@ fn proto_and_patch_actions_work_and_patch_history_is_atomic()
|
|||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let proto = proto.proto_json.expect("proto");
|
||||
assert_eq!(proto["kind"], "presentation");
|
||||
assert_eq!(proto["artifactId"], artifact_id);
|
||||
assert_eq!(proto["activeSlideId"], slide_id);
|
||||
assert_eq!(proto["commentAuthor"], serde_json::Value::Null);
|
||||
assert_eq!(proto["commentThreads"], serde_json::json!([]));
|
||||
assert_eq!(proto["masters"], serde_json::json!([]));
|
||||
assert_eq!(proto["layouts"], serde_json::json!([]));
|
||||
assert_eq!(proto["theme"]["hexColorMap"], serde_json::json!({}));
|
||||
assert_eq!(proto["styles"].as_array().map(Vec::len), Some(5));
|
||||
assert_eq!(proto["slides"].as_array().map(Vec::len), Some(1));
|
||||
assert_eq!(proto["slides"][0]["slideId"], slide_id);
|
||||
assert_eq!(proto["slides"][0]["backgroundFill"], "FFEECC");
|
||||
assert_eq!(
|
||||
proto.proto_json,
|
||||
Some(serde_json::json!({
|
||||
"kind": "presentation",
|
||||
"artifactId": artifact_id,
|
||||
"anchor": format!("pr/{artifact_id}"),
|
||||
"name": "Proto Patch",
|
||||
"slideSize": {
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": 720,
|
||||
"height": 540,
|
||||
"unit": "points"
|
||||
},
|
||||
"activeSlideIndex": 0,
|
||||
"activeSlideId": slide_id,
|
||||
"theme": {
|
||||
"colorScheme": {},
|
||||
"hexColorMap": {},
|
||||
"majorFont": serde_json::Value::Null,
|
||||
"minorFont": serde_json::Value::Null
|
||||
},
|
||||
"styles": [
|
||||
{
|
||||
"kind": "textStyle",
|
||||
"id": "st/body",
|
||||
"name": "body",
|
||||
"builtIn": true,
|
||||
"style": {
|
||||
"styleName": "body",
|
||||
"fontSize": 14,
|
||||
"fontFamily": serde_json::Value::Null,
|
||||
"color": serde_json::Value::Null,
|
||||
"alignment": "left",
|
||||
"bold": false,
|
||||
"italic": false,
|
||||
"underline": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "textStyle",
|
||||
"id": "st/heading1",
|
||||
"name": "heading1",
|
||||
"builtIn": true,
|
||||
"style": {
|
||||
"styleName": "heading1",
|
||||
"fontSize": 22,
|
||||
"fontFamily": serde_json::Value::Null,
|
||||
"color": serde_json::Value::Null,
|
||||
"alignment": "left",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
"underline": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "textStyle",
|
||||
"id": "st/list",
|
||||
"name": "list",
|
||||
"builtIn": true,
|
||||
"style": {
|
||||
"styleName": "list",
|
||||
"fontSize": 14,
|
||||
"fontFamily": serde_json::Value::Null,
|
||||
"color": serde_json::Value::Null,
|
||||
"alignment": "left",
|
||||
"bold": false,
|
||||
"italic": false,
|
||||
"underline": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "textStyle",
|
||||
"id": "st/numberedlist",
|
||||
"name": "numberedlist",
|
||||
"builtIn": true,
|
||||
"style": {
|
||||
"styleName": "numberedlist",
|
||||
"fontSize": 14,
|
||||
"fontFamily": serde_json::Value::Null,
|
||||
"color": serde_json::Value::Null,
|
||||
"alignment": "left",
|
||||
"bold": false,
|
||||
"italic": false,
|
||||
"underline": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "textStyle",
|
||||
"id": "st/title",
|
||||
"name": "title",
|
||||
"builtIn": true,
|
||||
"style": {
|
||||
"styleName": "title",
|
||||
"fontSize": 28,
|
||||
"fontFamily": serde_json::Value::Null,
|
||||
"color": serde_json::Value::Null,
|
||||
"alignment": "left",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
"underline": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"masters": [],
|
||||
"layouts": [],
|
||||
"slides": [
|
||||
{
|
||||
"slideId": slide_id,
|
||||
"anchor": format!("sl/{slide_id}"),
|
||||
"index": 0,
|
||||
"layoutId": serde_json::Value::Null,
|
||||
"backgroundFill": "FFEECC",
|
||||
"notes": {
|
||||
"anchor": format!("nt/{slide_id}"),
|
||||
"text": "",
|
||||
"visible": true,
|
||||
"textPreview": "",
|
||||
"textChars": 0,
|
||||
"textLines": 0
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"kind": "text",
|
||||
"elementId": element_id,
|
||||
"anchor": format!("sh/{element_id}"),
|
||||
"frame": {
|
||||
"left": 40,
|
||||
"top": 60,
|
||||
"width": 180,
|
||||
"height": 50,
|
||||
"unit": "points"
|
||||
},
|
||||
"text": "Patch text",
|
||||
"textPreview": "Patch text",
|
||||
"textChars": 10,
|
||||
"textLines": 1,
|
||||
"fill": serde_json::Value::Null,
|
||||
"style": {
|
||||
"styleName": serde_json::Value::Null,
|
||||
"fontSize": serde_json::Value::Null,
|
||||
"fontFamily": serde_json::Value::Null,
|
||||
"color": serde_json::Value::Null,
|
||||
"alignment": serde_json::Value::Null,
|
||||
"bold": false,
|
||||
"italic": false,
|
||||
"underline": false
|
||||
},
|
||||
"zOrder": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}))
|
||||
proto["slides"][0]["notes"]["richText"]["ranges"],
|
||||
serde_json::json!([])
|
||||
);
|
||||
assert_eq!(
|
||||
proto["slides"][0]["elements"].as_array().map(Vec::len),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(proto["slides"][0]["elements"][0]["elementId"], element_id);
|
||||
assert_eq!(proto["slides"][0]["elements"][0]["text"], "Patch text");
|
||||
assert_eq!(
|
||||
proto["slides"][0]["elements"][0]["richText"]["ranges"],
|
||||
serde_json::json!([])
|
||||
);
|
||||
|
||||
let undone = manager.execute(
|
||||
|
|
@ -3191,6 +3507,6 @@ fn proto_and_patch_actions_work_and_patch_history_is_atomic()
|
|||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(redone_proto.proto_json, proto.proto_json);
|
||||
assert_eq!(redone_proto.proto_json, Some(proto));
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ Supported actions:
|
|||
- `get_style`
|
||||
- `describe_styles`
|
||||
- `set_notes`
|
||||
- `set_notes_rich_text`
|
||||
- `append_notes`
|
||||
- `clear_notes`
|
||||
- `set_notes_visibility`
|
||||
|
|
@ -48,13 +49,25 @@ Supported actions:
|
|||
- `add_image`
|
||||
- `replace_image`
|
||||
- `add_table`
|
||||
- `update_table_style`
|
||||
- `style_table_block`
|
||||
- `update_table_cell`
|
||||
- `merge_table_cells`
|
||||
- `add_chart`
|
||||
- `update_chart`
|
||||
- `add_chart_series`
|
||||
- `update_text`
|
||||
- `set_rich_text`
|
||||
- `format_text_range`
|
||||
- `replace_text`
|
||||
- `insert_text_after`
|
||||
- `set_hyperlink`
|
||||
- `set_comment_author`
|
||||
- `add_comment_thread`
|
||||
- `add_comment_reply`
|
||||
- `toggle_comment_reaction`
|
||||
- `resolve_comment_thread`
|
||||
- `reopen_comment_thread`
|
||||
- `update_shape_style`
|
||||
- `bring_to_front`
|
||||
- `send_to_back`
|
||||
|
|
@ -73,7 +86,7 @@ Example edit:
|
|||
Example sequential batch:
|
||||
`{"actions":[{"action":"create","args":{"name":"Quarterly Update"}},{"action":"add_slide","args":{}},{"action":"add_text_shape","args":{"slide_index":0,"text":"Revenue up 24%","position":{"left":48,"top":72,"width":260,"height":80}}}]}`
|
||||
|
||||
Table creation also accepts optional `column_widths` and `row_heights` arrays in points when you need explicit table sizing instead of even splits.
|
||||
Table creation also accepts optional `column_widths` and `row_heights` arrays in points when you need explicit table sizing instead of even splits. Tables also support `style_options`, `borders`, and `right_to_left`, with `update_table_style` and `style_table_block` available for incremental styling after creation.
|
||||
|
||||
Example export:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"export_pptx","args":{"path":"artifacts/q2-update.pptx"}}]}`
|
||||
|
|
@ -94,7 +107,7 @@ Layout references in `create_layout.parent_layout_id`, `add_layout_placeholder.l
|
|||
`insert_slide` accepts `index` or `after_slide_index`. If neither is provided, the new slide is inserted immediately after the active slide, or appended if no active slide is set yet.
|
||||
|
||||
Example inspect:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"inspect","args":{"include":"deck,slide,textbox,shape,table,chart,image,notes,layoutList","exclude":"notes","search":"roadmap","max_chars":12000}}]}`
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"inspect","args":{"include":"deck,slide,textbox,shape,table,chart,image,notes,layoutList,textRange,comment","exclude":"notes","search":"roadmap","max_chars":12000}}]}`
|
||||
|
||||
Example inspect target window:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"inspect","args":{"include":"textbox","target":{"id":"sh/element_3","before_lines":1,"after_lines":1}}}]}`
|
||||
|
|
@ -107,6 +120,14 @@ Example proto export:
|
|||
|
||||
`to_proto` returns a full JSON snapshot of the current in-memory presentation document, including slide/layout records, anchors, notes, theme state, and typed element payloads.
|
||||
|
||||
Rich text is supported on notes, text boxes, shapes with text, and table cells. Use `set_rich_text` to replace a full rich-text payload, `set_notes_rich_text` for speaker notes, and `format_text_range` to annotate a substring by `query` or explicit codepoint range. `inspect`, `resolve`, and `to_proto` surface text-range anchors as `tr/<range_id>`.
|
||||
|
||||
Comment threads are supported through `set_comment_author`, `add_comment_thread`, `add_comment_reply`, `toggle_comment_reaction`, `resolve_comment_thread`, and `reopen_comment_thread`. Thread anchors resolve as `th/<thread_id>`, and comment records appear in both `inspect` and `to_proto`.
|
||||
|
||||
Charts support richer series metadata plus `update_chart` and `add_chart_series`, including legend, axis, data-label, marker, fill, and per-point override state.
|
||||
|
||||
Exported PPTX files embed Codex metadata so rich text, comment threads, and advanced table/chart state round-trip through `export_pptx` and `import_pptx` even when the base OOXML representation is lossy.
|
||||
|
||||
Example patch recording:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"record_patch","args":{"operations":[{"action":"add_text_shape","args":{"slide_index":0,"text":"Headline","position":{"left":48,"top":48,"width":320,"height":72}}},{"action":"set_slide_background","args":{"slide_index":0,"fill":"#F7F1E8"}}]}}]}`
|
||||
|
||||
|
|
@ -136,10 +157,19 @@ Text styling payloads on `add_text_shape`, `add_shape.text_style`, `update_text.
|
|||
|
||||
Text-bearing elements also support literal `replace_text` and `insert_text_after` helpers for in-place edits without resending the full string.
|
||||
|
||||
Example rich text update:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"set_rich_text","args":{"element_id":"element_3","text":[[{"run":"Quarterly ","text_style":{"bold":true}},"update pipeline"]],"text_layout":{"wrap":"square","auto_fit":"shrinkText","vertical_alignment":"middle","insets":{"left":6,"right":6,"top":4,"bottom":4}}}}]}`
|
||||
|
||||
Example substring formatting:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"format_text_range","args":{"element_id":"element_3","query":"update","styling":{"italic":true},"link":{"uri":"https://example.com/update","is_external":true}}}]}`
|
||||
|
||||
Text boxes and shapes support whole-element hyperlinks via `set_hyperlink`. Supported `link_type` values are `url`, `slide`, `first_slide`, `last_slide`, `next_slide`, `previous_slide`, `end_show`, `email`, and `file`. Use `clear: true` to remove an existing hyperlink.
|
||||
|
||||
Notes visibility is honored on export: `set_notes_visibility` controls whether speaker notes are emitted into exported PPTX output.
|
||||
|
||||
Example comment thread:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"set_comment_author","args":{"display_name":"Jamie Fox","initials":"JF"}},{"action":"add_comment_thread","args":{"slide_index":0,"element_id":"element_3","query":"Quarterly","text":"Tighten this headline"}},{"action":"add_comment_reply","args":{"thread_id":"thread_1","text":"Applied to the draft."}}]}`
|
||||
|
||||
Image placeholders can be prompt-only. `add_image` accepts `prompt` without `path`/`data_url`, and unresolved placeholders export as a visible placeholder box instead of failing.
|
||||
|
||||
Layout placeholders with `placeholder_type: "picture"` or `"image"` materialize as placeholder image elements on slides, so they appear in `list_slide_placeholders`, `inspect`, and `resolve` as `image` records rather than generic shapes.
|
||||
|
|
@ -158,6 +188,9 @@ Shape strokes accept an optional `style` field such as `solid`, `dashed`, `dotte
|
|||
|
||||
Connectors are supported via `add_connector`, with straight/elbow/curved types plus dash styles and arrow heads.
|
||||
|
||||
Example chart update:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"update_chart","args":{"element_id":"element_7","style_index":12,"legend_position":"bottom","y_axis_title":"USD (millions)"}},{"action":"add_chart_series","args":{"element_id":"element_7","name":"Target","values":[11,13],"fill":"#70AD47","marker":{"symbol":"diamond","size":6}}}]}`
|
||||
|
||||
Example preview:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"export_preview","args":{"slide_index":0,"path":"artifacts/q2-update-slide1.png"}}]}`
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue