use std::ffi::{CString, c_void};
use std::os::raw::{c_char, c_int};
use std::panic::{AssertUnwindSafe, catch_unwind};
use crate::ffi;
use crate::{Capabilities, Error, ExtKind, Format, RuntimeFormat, Span};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Description {
pub name: String,
pub caps: Capabilities,
pub max_mapping_depth: Option<u8>,
pub lossless: Option<NativeKinds>,
pub syntax: Option<Syntax>,
pub dialects: Vec<Dialect>,
pub samples: Vec<String>,
pub renderers: Renderers,
}
impl Description {
pub fn new(name: &str) -> Self {
Description {
name: name.to_owned(),
caps: Capabilities::default(),
dialects: vec![Dialect::new(name)],
..Default::default()
}
}
}
impl Default for Capabilities {
fn default() -> Self {
Capabilities::new(true, false, false)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Dialect {
pub name: String,
pub extensions: Vec<String>,
pub splice: Splice,
pub empty_doc_seed: Option<String>,
pub syntax: Option<Syntax>,
}
impl Dialect {
pub fn new(name: &str) -> Self {
Dialect {
name: name.to_owned(),
extensions: Vec::new(),
splice: Splice::Literal,
empty_doc_seed: None,
syntax: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Splice {
#[default]
Literal,
JsonString,
Raw,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct NativeKinds {
pub null: bool,
pub offset_datetime: bool,
pub local_datetime: bool,
pub local_date: bool,
pub local_time: bool,
pub enum_literal: bool,
pub char_literal: bool,
pub number_special: bool,
pub plist_date: bool,
pub plist_data: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Renderers {
pub value: bool,
pub entry: bool,
pub item: bool,
pub tail: bool,
pub key: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Syntax {
pub comments: Comments,
pub kv_sep: Option<String>,
pub flow_kv_sep_from_siblings: bool,
pub flow_map_pad: String,
pub key_style: KeyStyle,
pub key_sigil: Option<u8>,
pub empty_map_literal: Option<String>,
pub block_seq_editable: bool,
pub flow_containers: bool,
pub indent_unit: String,
pub seq_item_marker: String,
pub closed_containers: Option<ClosedContainers>,
pub single_line_block_mapping: bool,
pub bare_document_mapping: bool,
pub flow_map_open: String,
pub flow_map_close: String,
pub structural_indent: bool,
pub section_noun: Option<SectionNoun>,
pub section_header: Option<SectionHeader>,
pub merge_key: Option<String>,
}
impl Default for Syntax {
fn default() -> Self {
Syntax {
comments: Comments::hash(),
kv_sep: None,
flow_kv_sep_from_siblings: false,
flow_map_pad: String::new(),
key_style: KeyStyle::Verbatim,
key_sigil: None,
empty_map_literal: None,
block_seq_editable: true,
flow_containers: true,
indent_unit: " ".to_owned(),
seq_item_marker: "- ".to_owned(),
closed_containers: None,
single_line_block_mapping: false,
bare_document_mapping: true,
flow_map_open: "{".to_owned(),
flow_map_close: "}".to_owned(),
structural_indent: false,
section_noun: None,
section_header: None,
merge_key: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Comments {
pub style: CommentStyle,
pub line: Option<CommentDelimiter>,
pub trailing: Option<CommentDelimiter>,
}
impl Comments {
pub fn hash() -> Self {
Comments {
style: CommentStyle::Hash,
line: Some(CommentDelimiter::open("#")),
trailing: Some(CommentDelimiter::open("#")),
}
}
pub fn slashes() -> Self {
Comments {
style: CommentStyle::Slashes,
line: Some(CommentDelimiter::open("//")),
trailing: Some(CommentDelimiter::open("//")),
}
}
pub fn none() -> Self {
Comments {
style: CommentStyle::Hash,
line: None,
trailing: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum CommentStyle {
#[default]
Hash,
Slashes,
Semicolon,
XmlComment,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CommentDelimiter {
pub open: String,
pub close: String,
pub forbidden: Option<String>,
}
impl CommentDelimiter {
pub fn open(open: &str) -> Self {
CommentDelimiter {
open: open.to_owned(),
close: String::new(),
forbidden: None,
}
}
pub fn pair(open: &str, close: &str) -> Self {
CommentDelimiter {
open: open.to_owned(),
close: close.to_owned(),
forbidden: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum KeyStyle {
#[default]
Verbatim,
JsonQuoted,
ZonField,
BareOrQuoted,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SectionNoun {
Table,
Section,
Container,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SectionHeader {
pub open: String,
pub close: String,
pub seq_open: Option<String>,
pub seq_close: Option<String>,
pub sep: String,
pub skip_index: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClosedContainers {
pub map_open: String,
pub map_close: String,
pub seq_open: String,
pub seq_close: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct NodeTable {
pub rows: Vec<NodeRow>,
pub regions: Vec<RegionRow>,
pub mentions: Vec<MentionRow>,
pub comments: Vec<CommentRow>,
pub directives: Vec<DirectiveRow>,
}
impl NodeTable {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, row: NodeRow) -> u32 {
self.rows.push(row);
(self.rows.len() - 1) as u32
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct NodeRow {
pub kind: NodeKind,
pub ext_kind: Option<ExtKind>,
pub parent: Option<u32>,
pub span: Option<Span>,
pub text: Option<String>,
pub anchor: Option<String>,
pub anchor_span: Option<Span>,
pub tag: Option<String>,
pub tag_span: Option<Span>,
pub marker: Option<Span>,
pub sep: Option<Span>,
}
impl NodeRow {
pub fn new(kind: NodeKind, parent: Option<u32>, span: Span) -> Self {
NodeRow {
kind,
ext_kind: None,
parent,
span: Some(span),
text: None,
anchor: None,
anchor_span: None,
tag: None,
tag_span: None,
marker: None,
sep: None,
}
}
pub fn with_text(mut self, text: &str) -> Self {
self.text = Some(text.to_owned());
self
}
pub fn with_sep(mut self, sep: Span) -> Self {
self.sep = Some(sep);
self
}
pub fn with_marker(mut self, marker: Span) -> Self {
self.marker = Some(marker);
self
}
pub fn with_anchor(mut self, name: &str, span: Span) -> Self {
self.anchor = Some(name.to_owned());
self.anchor_span = Some(span);
self
}
pub fn with_tag(mut self, tag: &str, span: Span) -> Self {
self.tag = Some(tag.to_owned());
self.tag_span = Some(span);
self
}
pub fn with_ext_kind(mut self, kind: ExtKind) -> Self {
self.ext_kind = Some(kind);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NodeKind {
Null,
Bool,
Int,
Float,
String,
Sequence,
Mapping,
KeyValue,
Alias,
}
impl NodeKind {
pub(crate) fn to_c(self) -> c_int {
match self {
NodeKind::Null => 0,
NodeKind::Bool => 1,
NodeKind::Int => 2,
NodeKind::Float => 3,
NodeKind::String => 4,
NodeKind::Sequence => 5,
NodeKind::Mapping => 6,
NodeKind::KeyValue => 7,
NodeKind::Alias => 8,
}
}
pub(crate) fn from_c(v: c_int) -> Option<Self> {
Some(match v {
0 => NodeKind::Null,
1 => NodeKind::Bool,
2 => NodeKind::Int,
3 => NodeKind::Float,
4 => NodeKind::String,
5 => NodeKind::Sequence,
6 => NodeKind::Mapping,
7 => NodeKind::KeyValue,
8 => NodeKind::Alias,
_ => return None,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RegionRow {
pub node: u32,
pub start: usize,
pub end: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MentionRow {
pub node: u32,
pub span: Span,
pub kind: MentionKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MentionKind {
Header,
Entry,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CommentRow {
pub node: u32,
pub slot: CommentSlot,
pub style: CommentForm,
pub text: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DirectiveRow {
pub handle: String,
pub prefix: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CommentSlot {
Leading,
Trailing,
Dangling,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CommentForm {
Line,
Block,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct PrintOptions {
pub pretty: bool,
pub strip_comments: bool,
pub indent: u8,
pub width: u16,
}
impl Default for PrintOptions {
fn default() -> Self {
PrintOptions {
pretty: true,
strip_comments: false,
indent: 2,
width: 80,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LanguageError {
pub message: String,
pub byte_offset: Option<usize>,
}
impl LanguageError {
pub fn new(message: impl Into<String>) -> Self {
LanguageError {
message: message.into(),
byte_offset: None,
}
}
pub fn at(message: impl Into<String>, byte_offset: usize) -> Self {
LanguageError {
message: message.into(),
byte_offset: Some(byte_offset),
}
}
}
impl std::fmt::Display for LanguageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.byte_offset {
Some(off) => write!(f, "{} (byte offset {off})", self.message),
None => f.write_str(&self.message),
}
}
}
impl std::error::Error for LanguageError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Renderer {
Value,
Entry,
Item,
Tail,
Key,
}
impl Renderer {
pub fn name(self) -> &'static str {
match self {
Renderer::Value => "value",
Renderer::Entry => "entry",
Renderer::Item => "item",
Renderer::Tail => "tail",
Renderer::Key => "key",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Literal {
Null,
Bool,
Int,
Float,
Datetime,
#[default]
String,
}
impl Literal {
pub fn name(self) -> &'static str {
match self {
Literal::Null => "null",
Literal::Bool => "bool",
Literal::Int => "int",
Literal::Float => "float",
Literal::Datetime => "datetime",
Literal::String => "string",
}
}
pub fn from_name(name: &str) -> Option<Literal> {
Some(match name {
"null" => Literal::Null,
"bool" => Literal::Bool,
"int" => Literal::Int,
"float" => Literal::Float,
"datetime" => Literal::Datetime,
"string" => Literal::String,
_ => return None,
})
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RenderArgs<'a> {
pub dialect: &'a str,
pub indent: &'a [u8],
pub key: &'a [u8],
pub value: &'a [u8],
pub literal: Literal,
pub old_key: &'a [u8],
}
pub trait Language: Send + Sync + 'static {
fn describe(&self) -> Description;
fn parse(&self, dialect: &str, input: &[u8]) -> Result<NodeTable, LanguageError>;
fn print(
&self,
dialect: &str,
table: &NodeTable,
options: &PrintOptions,
) -> Result<Vec<u8>, LanguageError> {
let _ = (dialect, table, options);
Err(LanguageError::new("this language does not print"))
}
fn render(&self, which: Renderer, args: RenderArgs<'_>) -> Result<Vec<u8>, LanguageError> {
let _ = args;
Err(LanguageError::new(format!(
"this language does not render {}",
which.name()
)))
}
}
pub fn register(lang: impl Language) -> Result<Vec<Format>, Error> {
let desc = lang.describe();
let dialect_count = desc.dialects.len();
let reg = Registration::new(Box::new(lang), desc)?;
let reg: &'static Registration = Box::leak(Box::new(reg));
let vt = reg.vtable();
let mut format: c_int = -1;
let mut err = ffi::FigError::new();
let status = unsafe { ffi::fig_language_register(&vt, &mut format, &mut err) };
if status != ffi::FigStatus(ffi::FigStatus::OK) {
let len = err.message_len.min(err.message.len());
let message = String::from_utf8_lossy(&err.message[..len]).into_owned();
if message.is_empty() {
Error::from_status(status)?;
return Err(Error::Internal);
}
return Err(Error::Language(message));
}
Ok((0..dialect_count as c_int)
.map(|i| Format::Runtime(RuntimeFormat(format + i)))
.collect())
}
struct Registration {
lang: Box<dyn Language>,
name: CString,
caps: u32,
max_mapping_depth: c_int,
lossless: Option<Box<ffi::FigNativeKinds>>,
syntax: Option<Box<CSyntax>>,
dialects: Vec<ffi::FigDialectDesc>,
_dialect_owned: Vec<CDialect>,
samples: Vec<Vec<u8>>,
sample_strs: Vec<ffi::FigStr>,
renderers: Renderers,
}
struct CSyntax {
c: ffi::FigSyntax,
_strings: Vec<CString>,
}
struct CDialect {
_name: CString,
_extensions: Vec<CString>,
_extension_ptrs: Vec<*const c_char>,
_seed: Option<CString>,
_syntax: Option<Box<CSyntax>>,
}
unsafe impl Send for Registration {}
unsafe impl Sync for Registration {}
fn cstr(s: &str) -> Result<CString, Error> {
CString::new(s)
.map_err(|_| Error::Language(format!("a declared string contains a NUL byte: {s:?}")))
}
fn opt_ptr(strings: &mut Vec<CString>, s: Option<&str>) -> Result<*const c_char, Error> {
match s {
Some(s) => {
let c = cstr(s)?;
let p = c.as_ptr();
strings.push(c);
Ok(p)
}
None => Ok(std::ptr::null()),
}
}
impl CSyntax {
fn new(s: &Syntax) -> Result<Box<Self>, Error> {
let mut strings = Vec::new();
let delim = |strings: &mut Vec<CString>,
d: Option<&CommentDelimiter>|
-> Result<ffi::FigCommentDelimiter, Error> {
Ok(match d {
Some(d) => ffi::FigCommentDelimiter {
open: opt_ptr(strings, Some(&d.open))?,
close: opt_ptr(strings, Some(&d.close))?,
forbidden: opt_ptr(strings, d.forbidden.as_deref())?,
},
None => ffi::FigCommentDelimiter {
open: std::ptr::null(),
close: std::ptr::null(),
forbidden: std::ptr::null(),
},
})
};
let c = ffi::FigSyntax {
comments: ffi::FigComments {
style: match s.comments.style {
CommentStyle::Hash => 0,
CommentStyle::Slashes => 1,
CommentStyle::Semicolon => 2,
CommentStyle::XmlComment => 3,
},
line: delim(&mut strings, s.comments.line.as_ref())?,
trailing: delim(&mut strings, s.comments.trailing.as_ref())?,
},
kv_sep: opt_ptr(&mut strings, s.kv_sep.as_deref())?,
flow_kv_sep_from_siblings: s.flow_kv_sep_from_siblings,
flow_map_pad: opt_ptr(&mut strings, Some(&s.flow_map_pad))?,
key_style: match s.key_style {
KeyStyle::Verbatim => 0,
KeyStyle::JsonQuoted => 1,
KeyStyle::ZonField => 2,
KeyStyle::BareOrQuoted => 3,
},
key_sigil: s.key_sigil.unwrap_or(0),
empty_map_literal: opt_ptr(&mut strings, s.empty_map_literal.as_deref())?,
block_seq_editable: s.block_seq_editable,
flow_containers: s.flow_containers,
indent_unit: opt_ptr(&mut strings, Some(&s.indent_unit))?,
seq_item_marker: opt_ptr(&mut strings, Some(&s.seq_item_marker))?,
closed_containers: match &s.closed_containers {
Some(c) => ffi::FigClosedContainers {
map_open: opt_ptr(&mut strings, Some(&c.map_open))?,
map_close: opt_ptr(&mut strings, Some(&c.map_close))?,
seq_open: opt_ptr(&mut strings, Some(&c.seq_open))?,
seq_close: opt_ptr(&mut strings, Some(&c.seq_close))?,
},
None => ffi::FigClosedContainers {
map_open: std::ptr::null(),
map_close: std::ptr::null(),
seq_open: std::ptr::null(),
seq_close: std::ptr::null(),
},
},
single_line_block_mapping: s.single_line_block_mapping,
bare_document_mapping: s.bare_document_mapping,
flow_map_open: opt_ptr(&mut strings, Some(&s.flow_map_open))?,
flow_map_close: opt_ptr(&mut strings, Some(&s.flow_map_close))?,
structural_indent: s.structural_indent,
section_noun: match s.section_noun {
None => -1,
Some(SectionNoun::Table) => 0,
Some(SectionNoun::Section) => 1,
Some(SectionNoun::Container) => 2,
},
section_header: match &s.section_header {
Some(h) => ffi::FigSectionHeader {
open: opt_ptr(&mut strings, Some(&h.open))?,
close: opt_ptr(&mut strings, Some(&h.close))?,
seq_open: opt_ptr(&mut strings, h.seq_open.as_deref())?,
seq_close: opt_ptr(&mut strings, h.seq_close.as_deref())?,
sep: opt_ptr(&mut strings, Some(&h.sep))?,
skip_index: h.skip_index,
},
None => ffi::FigSectionHeader {
open: std::ptr::null(),
close: std::ptr::null(),
seq_open: std::ptr::null(),
seq_close: std::ptr::null(),
sep: std::ptr::null(),
skip_index: true,
},
},
merge_key: opt_ptr(&mut strings, s.merge_key.as_deref())?,
};
Ok(Box::new(CSyntax {
c,
_strings: strings,
}))
}
}
impl Registration {
fn new(lang: Box<dyn Language>, desc: Description) -> Result<Self, Error> {
let name = cstr(&desc.name)?;
let mut caps = 0u32;
if desc.caps.read {
caps |= 1 << 0;
}
if desc.caps.edit {
caps |= 1 << 1;
}
if desc.caps.serialize {
caps |= 1 << 2;
}
if desc.caps.references {
caps |= 1 << 3;
}
let lossless = desc.lossless.map(|l| {
Box::new(ffi::FigNativeKinds {
null_: l.null,
offset_datetime: l.offset_datetime,
local_datetime: l.local_datetime,
local_date: l.local_date,
local_time: l.local_time,
enum_literal: l.enum_literal,
char_literal: l.char_literal,
number_special: l.number_special,
plist_date: l.plist_date,
plist_data: l.plist_data,
})
});
let syntax = match &desc.syntax {
Some(s) => Some(CSyntax::new(s)?),
None => None,
};
let mut dialects = Vec::with_capacity(desc.dialects.len());
let mut owned = Vec::with_capacity(desc.dialects.len());
for d in &desc.dialects {
let dname = cstr(&d.name)?;
let extensions: Vec<CString> = d
.extensions
.iter()
.map(|e| cstr(e))
.collect::<Result<_, _>>()?;
let mut extension_ptrs: Vec<*const c_char> =
extensions.iter().map(|e| e.as_ptr()).collect();
extension_ptrs.push(std::ptr::null());
let seed = match &d.empty_doc_seed {
Some(s) => Some(cstr(s)?),
None => None,
};
let dsyntax = match &d.syntax {
Some(s) => Some(CSyntax::new(s)?),
None => None,
};
dialects.push(ffi::FigDialectDesc {
name: dname.as_ptr(),
extensions: extension_ptrs.as_ptr(),
splice: match d.splice {
Splice::Literal => 0,
Splice::JsonString => 1,
Splice::Raw => 2,
},
empty_doc_seed: seed.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
syntax: dsyntax
.as_ref()
.map_or(std::ptr::null(), |s| &s.c as *const _),
});
owned.push(CDialect {
_name: dname,
_extensions: extensions,
_extension_ptrs: extension_ptrs,
_seed: seed,
_syntax: dsyntax,
});
}
let samples: Vec<Vec<u8>> = desc.samples.iter().map(|s| s.as_bytes().to_vec()).collect();
let sample_strs = samples
.iter()
.map(|s| ffi::FigStr {
ptr: s.as_ptr(),
len: s.len(),
})
.collect();
Ok(Registration {
lang,
name,
caps,
max_mapping_depth: desc
.max_mapping_depth
.map_or(ffi::FIG_DEPTH_NONE, c_int::from),
lossless,
syntax,
dialects,
_dialect_owned: owned,
samples,
sample_strs,
renderers: desc.renderers,
})
}
fn vtable(&'static self) -> ffi::FigLanguageVTable {
ffi::FigLanguageVTable {
version: ffi::FIG_LANGUAGE_VTABLE_VERSION,
ctx: self as *const Registration as *mut c_void,
name: self.name.as_ptr(),
caps: self.caps,
max_mapping_depth: self.max_mapping_depth,
lossless: self
.lossless
.as_ref()
.map_or(std::ptr::null(), |l| &**l as *const _),
syntax: self
.syntax
.as_ref()
.map_or(std::ptr::null(), |s| &s.c as *const _),
dialects: self.dialects.as_ptr(),
dialect_count: self.dialects.len(),
samples: self.sample_strs.as_ptr(),
sample_count: self.samples.len(),
parse: parse_thunk,
print: if self.caps & (1 << 2) != 0 {
Some(print_thunk)
} else {
None
},
free_table: free_table_thunk,
free_bytes: free_bytes_thunk,
render_value: if self.renderers.value {
Some(render_value_thunk)
} else {
None
},
render_entry: if self.renderers.entry {
Some(render_entry_thunk)
} else {
None
},
render_item: if self.renderers.item {
Some(render_item_thunk)
} else {
None
},
render_tail: if self.renderers.tail {
Some(render_tail_thunk)
} else {
None
},
render_key: if self.renderers.key {
Some(render_key_thunk)
} else {
None
},
}
}
}
fn fill_err(err: *mut ffi::FigError, e: &LanguageError) {
if err.is_null() {
return;
}
let err = unsafe { &mut *err };
let bytes = e.message.as_bytes();
let n = bytes.len().min(err.message.len() - 1);
err.message[..n].copy_from_slice(&bytes[..n]);
err.message[n] = 0;
err.message_len = n;
err.byte_offset = e.byte_offset.unwrap_or(0);
}
fn panic_message(p: Box<dyn std::any::Any + Send>) -> LanguageError {
let msg = p
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| p.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "panic".to_owned());
LanguageError::new(format!("the language panicked: {msg}"))
}
unsafe fn reg_of<'a>(ctx: *mut c_void) -> &'a Registration {
unsafe { &*(ctx as *const Registration) }
}
unsafe fn dialect_of<'a>(dialect: *const c_char) -> &'a str {
unsafe { std::ffi::CStr::from_ptr(dialect) }
.to_str()
.unwrap_or("")
}
fn bytes_of<'a>(s: ffi::FigStr) -> &'a [u8] {
if s.len == ffi::FIG_LEN_NONE || s.len == 0 || s.ptr.is_null() {
return &[];
}
unsafe { std::slice::from_raw_parts(s.ptr, s.len) }
}
struct TableHolder {
_table: NodeTable,
rows: Vec<ffi::FigNodeRow>,
regions: Vec<ffi::FigRegionRow>,
mentions: Vec<ffi::FigMentionRow>,
comments: Vec<ffi::FigCommentRow>,
directives: Vec<ffi::FigDirectiveRow>,
}
fn str_of(s: &Option<String>) -> ffi::FigStr {
match s {
Some(s) => ffi::FigStr {
ptr: s.as_ptr(),
len: s.len(),
},
None => ffi::FigStr::NONE,
}
}
fn span_of(s: Option<Span>) -> ffi::FigSpan {
match s {
Some(s) => ffi::FigSpan {
start: s.start,
end: s.end,
},
None => ffi::FigSpan::NONE,
}
}
fn table_to_c(table: NodeTable) -> Box<TableHolder> {
let rows = table
.rows
.iter()
.map(|r| ffi::FigNodeRow {
kind: r.kind.to_c(),
ext_kind: r.ext_kind.map_or(ffi::FIG_EXT_NONE, |k| k.to_c()),
parent: r.parent.unwrap_or(ffi::FIG_ROW_NONE),
span: span_of(r.span),
text: str_of(&r.text),
anchor: str_of(&r.anchor),
anchor_span: span_of(r.anchor_span),
tag: str_of(&r.tag),
tag_span: span_of(r.tag_span),
marker: span_of(r.marker),
sep: span_of(r.sep),
})
.collect();
let regions = table
.regions
.iter()
.map(|r| ffi::FigRegionRow {
node: r.node,
start: r.start,
end: r.end,
})
.collect();
let mentions = table
.mentions
.iter()
.map(|m| ffi::FigMentionRow {
node: m.node,
span: span_of(Some(m.span)),
kind: match m.kind {
MentionKind::Header => ffi::FIG_MENTION_HEADER,
MentionKind::Entry => ffi::FIG_MENTION_ENTRY,
},
})
.collect();
let comments = table
.comments
.iter()
.map(|c| ffi::FigCommentRow {
node: c.node,
slot: match c.slot {
CommentSlot::Leading => ffi::FIG_COMMENT_LEADING,
CommentSlot::Trailing => ffi::FIG_COMMENT_TRAILING,
CommentSlot::Dangling => ffi::FIG_COMMENT_DANGLING,
},
style: match c.style {
CommentForm::Line => ffi::FIG_COMMENT_LINE,
CommentForm::Block => ffi::FIG_COMMENT_BLOCK,
},
text: ffi::FigStr {
ptr: c.text.as_ptr(),
len: c.text.len(),
},
})
.collect();
let directives = table
.directives
.iter()
.map(|d| ffi::FigDirectiveRow {
handle: ffi::FigStr {
ptr: d.handle.as_ptr(),
len: d.handle.len(),
},
prefix: ffi::FigStr {
ptr: d.prefix.as_ptr(),
len: d.prefix.len(),
},
})
.collect();
Box::new(TableHolder {
_table: table,
rows,
regions,
mentions,
comments,
directives,
})
}
impl TableHolder {
fn c_table(&self, owner: *mut c_void) -> ffi::FigNodeTable {
ffi::FigNodeTable {
rows: self.rows.as_ptr(),
row_count: self.rows.len(),
regions: self.regions.as_ptr(),
region_count: self.regions.len(),
mentions: self.mentions.as_ptr(),
mention_count: self.mentions.len(),
comments: self.comments.as_ptr(),
comment_count: self.comments.len(),
directives: self.directives.as_ptr(),
directive_count: self.directives.len(),
owner,
}
}
}
pub(crate) fn table_from_c(t: &ffi::FigNodeTable) -> Result<NodeTable, LanguageError> {
let text_of = |s: ffi::FigStr| -> Result<Option<String>, LanguageError> {
if s.len == ffi::FIG_LEN_NONE {
return Ok(None);
}
String::from_utf8(bytes_of(s).to_vec())
.map(Some)
.map_err(|_| LanguageError::new("a table string is not UTF-8"))
};
let span_of = |s: ffi::FigSpan| -> Option<Span> {
if s.start == ffi::FIG_OFFSET_NONE {
None
} else {
Some(Span {
start: s.start,
end: s.end,
})
}
};
let rows = if t.rows.is_null() {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(t.rows, t.row_count) }
};
let regions = if t.regions.is_null() {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(t.regions, t.region_count) }
};
let mentions = if t.mentions.is_null() {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(t.mentions, t.mention_count) }
};
let comments = if t.comments.is_null() {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(t.comments, t.comment_count) }
};
let directives = if t.directives.is_null() {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(t.directives, t.directive_count) }
};
let mut out = NodeTable::new();
for r in rows {
out.rows.push(NodeRow {
kind: NodeKind::from_c(r.kind)
.ok_or_else(|| LanguageError::new("unknown node kind"))?,
ext_kind: if r.ext_kind == ffi::FIG_EXT_NONE {
None
} else {
ExtKind::from_c(r.ext_kind)
},
parent: if r.parent == ffi::FIG_ROW_NONE {
None
} else {
Some(r.parent)
},
span: span_of(r.span),
text: text_of(r.text)?,
anchor: text_of(r.anchor)?,
anchor_span: span_of(r.anchor_span),
tag: text_of(r.tag)?,
tag_span: span_of(r.tag_span),
marker: span_of(r.marker),
sep: span_of(r.sep),
});
}
for r in regions {
out.regions.push(RegionRow {
node: r.node,
start: r.start,
end: r.end,
});
}
for m in mentions {
out.mentions.push(MentionRow {
node: m.node,
span: span_of(m.span).ok_or_else(|| LanguageError::new("a mention has no span"))?,
kind: if m.kind == ffi::FIG_MENTION_ENTRY {
MentionKind::Entry
} else {
MentionKind::Header
},
});
}
for c in comments {
out.comments.push(CommentRow {
node: c.node,
slot: match c.slot {
ffi::FIG_COMMENT_TRAILING => CommentSlot::Trailing,
ffi::FIG_COMMENT_DANGLING => CommentSlot::Dangling,
_ => CommentSlot::Leading,
},
style: if c.style == ffi::FIG_COMMENT_BLOCK {
CommentForm::Block
} else {
CommentForm::Line
},
text: text_of(c.text)?.unwrap_or_default(),
});
}
for d in directives {
out.directives.push(DirectiveRow {
handle: text_of(d.handle)?
.ok_or_else(|| LanguageError::new("a directive has no handle"))?,
prefix: text_of(d.prefix)?
.ok_or_else(|| LanguageError::new("a directive has no prefix"))?,
});
}
Ok(out)
}
unsafe extern "C" fn parse_thunk(
ctx: *mut c_void,
dialect: *const c_char,
input: ffi::FigStr,
out: *mut ffi::FigNodeTable,
err: *mut ffi::FigError,
) -> c_int {
let reg = unsafe { reg_of(ctx) };
let dialect = unsafe { dialect_of(dialect) };
let input = bytes_of(input);
let result = catch_unwind(AssertUnwindSafe(|| reg.lang.parse(dialect, input)));
match result {
Ok(Ok(table)) => {
let holder = table_to_c(table);
let owner = Box::into_raw(holder);
unsafe { *out = (*owner).c_table(owner as *mut c_void) };
0
}
Ok(Err(e)) => {
fill_err(err, &e);
ffi::FigStatus::PARSE_ERROR
}
Err(p) => {
fill_err(err, &panic_message(p));
ffi::FigStatus::INTERNAL_ERROR
}
}
}
unsafe extern "C" fn free_table_thunk(_ctx: *mut c_void, table: *mut ffi::FigNodeTable) {
let owner = unsafe { (*table).owner } as *mut TableHolder;
if !owner.is_null() {
drop(unsafe { Box::from_raw(owner) });
}
}
fn give_bytes(bytes: Vec<u8>, out: *mut ffi::FigStr) {
let boxed = bytes.into_boxed_slice();
let len = boxed.len();
let ptr = Box::into_raw(boxed) as *const u8;
unsafe { *out = ffi::FigStr { ptr, len } };
}
unsafe extern "C" fn free_bytes_thunk(_ctx: *mut c_void, bytes: ffi::FigStr) {
if bytes.ptr.is_null() || bytes.len == ffi::FIG_LEN_NONE {
return;
}
let slice = std::ptr::slice_from_raw_parts_mut(bytes.ptr as *mut u8, bytes.len);
drop(unsafe { Box::from_raw(slice) });
}
unsafe extern "C" fn print_thunk(
ctx: *mut c_void,
dialect: *const c_char,
table: *const ffi::FigNodeTable,
options: *const ffi::FigPrintOptions,
out: *mut ffi::FigStr,
err: *mut ffi::FigError,
) -> c_int {
let reg = unsafe { reg_of(ctx) };
let dialect = unsafe { dialect_of(dialect) };
let opts = unsafe { &*options };
let options = PrintOptions {
pretty: opts.pretty,
strip_comments: opts.strip_comments,
indent: opts.indent,
width: opts.width,
};
let result = catch_unwind(AssertUnwindSafe(|| {
let table = table_from_c(unsafe { &*table })?;
reg.lang.print(dialect, &table, &options)
}));
match result {
Ok(Ok(bytes)) => {
give_bytes(bytes, out);
0
}
Ok(Err(e)) => {
fill_err(err, &e);
ffi::FigStatus::UNSUPPORTED_FORMAT
}
Err(p) => {
fill_err(err, &panic_message(p));
ffi::FigStatus::INTERNAL_ERROR
}
}
}
fn render_thunk_body(
ctx: *mut c_void,
which: Renderer,
dialect: *const c_char,
args: RenderArgs<'_>,
out: *mut ffi::FigStr,
err: *mut ffi::FigError,
) -> c_int {
let reg = unsafe { reg_of(ctx) };
let args = RenderArgs {
dialect: unsafe { dialect_of(dialect) },
..args
};
match catch_unwind(AssertUnwindSafe(|| reg.lang.render(which, args))) {
Ok(Ok(bytes)) => {
give_bytes(bytes, out);
0
}
Ok(Err(e)) => {
fill_err(err, &e);
ffi::FigStatus::UNSUPPORTED_OPERATION
}
Err(p) => {
fill_err(err, &panic_message(p));
ffi::FigStatus::INTERNAL_ERROR
}
}
}
unsafe extern "C" fn render_value_thunk(
ctx: *mut c_void,
dialect: *const c_char,
value: ffi::FigStr,
literal: *const c_char,
out: *mut ffi::FigStr,
err: *mut ffi::FigError,
) -> c_int {
let literal = Literal::from_name(unsafe { dialect_of(literal) }).unwrap_or_default();
render_thunk_body(
ctx,
Renderer::Value,
dialect,
RenderArgs {
value: bytes_of(value),
literal,
..Default::default()
},
out,
err,
)
}
unsafe extern "C" fn render_entry_thunk(
ctx: *mut c_void,
dialect: *const c_char,
indent: ffi::FigStr,
key: ffi::FigStr,
value: ffi::FigStr,
out: *mut ffi::FigStr,
err: *mut ffi::FigError,
) -> c_int {
render_thunk_body(
ctx,
Renderer::Entry,
dialect,
RenderArgs {
indent: bytes_of(indent),
key: bytes_of(key),
value: bytes_of(value),
..Default::default()
},
out,
err,
)
}
unsafe extern "C" fn render_item_thunk(
ctx: *mut c_void,
dialect: *const c_char,
indent: ffi::FigStr,
value: ffi::FigStr,
out: *mut ffi::FigStr,
err: *mut ffi::FigError,
) -> c_int {
render_thunk_body(
ctx,
Renderer::Item,
dialect,
RenderArgs {
indent: bytes_of(indent),
value: bytes_of(value),
..Default::default()
},
out,
err,
)
}
unsafe extern "C" fn render_tail_thunk(
ctx: *mut c_void,
dialect: *const c_char,
indent: ffi::FigStr,
key: ffi::FigStr,
value: ffi::FigStr,
out: *mut ffi::FigStr,
err: *mut ffi::FigError,
) -> c_int {
render_thunk_body(
ctx,
Renderer::Tail,
dialect,
RenderArgs {
indent: bytes_of(indent),
key: bytes_of(key),
value: bytes_of(value),
..Default::default()
},
out,
err,
)
}
unsafe extern "C" fn render_key_thunk(
ctx: *mut c_void,
dialect: *const c_char,
indent: ffi::FigStr,
key: ffi::FigStr,
old_key: ffi::FigStr,
out: *mut ffi::FigStr,
err: *mut ffi::FigError,
) -> c_int {
render_thunk_body(
ctx,
Renderer::Key,
dialect,
RenderArgs {
indent: bytes_of(indent),
key: bytes_of(key),
old_key: bytes_of(old_key),
..Default::default()
},
out,
err,
)
}