Skip to content

Commit 18ed243

Browse files
committed
Add missing Chromium form fields with DDD value objects
Add waitForSelector, emulatedMediaFeatures, failOnHttpStatusCodes, failOnResourceHttpStatusCodes, ignoreResourceHttpStatusDomains, and failOnResourceLoadingFailed to HtmlConversionBehaviors facet. Introduce validated DDD value objects (CssSelector, EmulatedMediaFeature, HttpStatusCode, DomainName) to prevent primitive obsession and enforce domain constraints at construction time. Serialization to Gotenberg's wire format is handled internally by FacetBase.
1 parent 09b6c02 commit 18ed243

14 files changed

Lines changed: 1146 additions & 0 deletions

File tree

src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/HtmlConversionBehaviorBuilder.cs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
using Gotenberg.Sharp.API.Client.Domain.ValueObjects;
17+
1618
using Newtonsoft.Json.Linq;
1719

1820
namespace Gotenberg.Sharp.API.Client.Domain.Builders.Faceted;
@@ -174,6 +176,155 @@ public HtmlConversionBehaviorBuilder SkipNetworkIdleEvent()
174176
return this;
175177
}
176178

179+
/// <summary>
180+
/// Sets a CSS selector to wait for before conversion.
181+
/// Chromium will delay conversion until the specified element appears in the DOM.
182+
/// </summary>
183+
/// <param name="selector">A validated CSS selector.</param>
184+
/// <returns>The builder instance for method chaining.</returns>
185+
public HtmlConversionBehaviorBuilder SetWaitForSelector(CssSelector selector)
186+
{
187+
_htmlConversionBehaviors.WaitForSelector = selector ?? throw new ArgumentNullException(nameof(selector));
188+
189+
return this;
190+
}
191+
192+
/// <summary>
193+
/// Sets a CSS selector to wait for before conversion.
194+
/// Chromium will delay conversion until the specified element appears in the DOM.
195+
/// </summary>
196+
/// <param name="selector">A CSS selector string (e.g., "#content", ".loaded").</param>
197+
/// <returns>The builder instance for method chaining.</returns>
198+
public HtmlConversionBehaviorBuilder SetWaitForSelector(string selector)
199+
{
200+
return SetWaitForSelector(CssSelector.Create(selector));
201+
}
202+
203+
/// <summary>
204+
/// Adds a CSS media feature override for Chromium rendering.
205+
/// </summary>
206+
/// <param name="feature">A validated emulated media feature.</param>
207+
/// <returns>The builder instance for method chaining.</returns>
208+
public HtmlConversionBehaviorBuilder AddEmulatedMediaFeature(EmulatedMediaFeature feature)
209+
{
210+
if (feature == null) throw new ArgumentNullException(nameof(feature));
211+
212+
_htmlConversionBehaviors.EmulatedMediaFeatures ??= new List<EmulatedMediaFeature>();
213+
_htmlConversionBehaviors.EmulatedMediaFeatures.Add(feature);
214+
215+
return this;
216+
}
217+
218+
/// <summary>
219+
/// Adds a CSS media feature override by name and value.
220+
/// </summary>
221+
/// <param name="name">CSS media feature name (e.g., "prefers-color-scheme").</param>
222+
/// <param name="value">CSS media feature value (e.g., "dark").</param>
223+
/// <returns>The builder instance for method chaining.</returns>
224+
public HtmlConversionBehaviorBuilder AddEmulatedMediaFeature(string name, string value)
225+
{
226+
return AddEmulatedMediaFeature(EmulatedMediaFeature.Create(name, value));
227+
}
228+
229+
/// <summary>
230+
/// Sets HTTP status codes that trigger a 409 Conflict when the main page returns them.
231+
/// </summary>
232+
/// <param name="statusCodes">Validated HTTP status codes.</param>
233+
/// <returns>The builder instance for method chaining.</returns>
234+
public HtmlConversionBehaviorBuilder SetFailOnHttpStatusCodes(IEnumerable<HttpStatusCode> statusCodes)
235+
{
236+
if (statusCodes == null) throw new ArgumentNullException(nameof(statusCodes));
237+
238+
_htmlConversionBehaviors.FailOnHttpStatusCodes = statusCodes.ToList();
239+
240+
return this;
241+
}
242+
243+
/// <summary>
244+
/// Sets HTTP status codes that trigger a 409 Conflict when the main page returns them.
245+
/// </summary>
246+
/// <param name="statusCodes">Raw HTTP status code integers (must be 100-599).</param>
247+
/// <returns>The builder instance for method chaining.</returns>
248+
public HtmlConversionBehaviorBuilder SetFailOnHttpStatusCodes(params int[] statusCodes)
249+
{
250+
return SetFailOnHttpStatusCodes(statusCodes.Select(HttpStatusCode.Create));
251+
}
252+
253+
/// <summary>
254+
/// Sets HTTP status codes that trigger a failure when page resources return them.
255+
/// </summary>
256+
/// <param name="statusCodes">Validated HTTP status codes.</param>
257+
/// <returns>The builder instance for method chaining.</returns>
258+
public HtmlConversionBehaviorBuilder SetFailOnResourceHttpStatusCodes(IEnumerable<HttpStatusCode> statusCodes)
259+
{
260+
if (statusCodes == null) throw new ArgumentNullException(nameof(statusCodes));
261+
262+
_htmlConversionBehaviors.FailOnResourceHttpStatusCodes = statusCodes.ToList();
263+
264+
return this;
265+
}
266+
267+
/// <summary>
268+
/// Sets HTTP status codes that trigger a failure when page resources return them.
269+
/// </summary>
270+
/// <param name="statusCodes">Raw HTTP status code integers (must be 100-599).</param>
271+
/// <returns>The builder instance for method chaining.</returns>
272+
public HtmlConversionBehaviorBuilder SetFailOnResourceHttpStatusCodes(params int[] statusCodes)
273+
{
274+
return SetFailOnResourceHttpStatusCodes(statusCodes.Select(HttpStatusCode.Create));
275+
}
276+
277+
/// <summary>
278+
/// Adds a domain to exclude from HTTP status code checks on resources.
279+
/// </summary>
280+
/// <param name="domain">A validated domain name.</param>
281+
/// <returns>The builder instance for method chaining.</returns>
282+
public HtmlConversionBehaviorBuilder AddIgnoreResourceHttpStatusDomain(DomainName domain)
283+
{
284+
if (domain == null) throw new ArgumentNullException(nameof(domain));
285+
286+
_htmlConversionBehaviors.IgnoreResourceHttpStatusDomains ??= new List<DomainName>();
287+
_htmlConversionBehaviors.IgnoreResourceHttpStatusDomains.Add(domain);
288+
289+
return this;
290+
}
291+
292+
/// <summary>
293+
/// Adds a domain to exclude from HTTP status code checks on resources.
294+
/// </summary>
295+
/// <param name="domain">A domain string (e.g., "cdn.example.com").</param>
296+
/// <returns>The builder instance for method chaining.</returns>
297+
public HtmlConversionBehaviorBuilder AddIgnoreResourceHttpStatusDomain(string domain)
298+
{
299+
return AddIgnoreResourceHttpStatusDomain(DomainName.Create(domain));
300+
}
301+
302+
/// <summary>
303+
/// Adds multiple domains to exclude from HTTP status code checks on resources.
304+
/// </summary>
305+
/// <param name="domains">Domain strings to exclude.</param>
306+
/// <returns>The builder instance for method chaining.</returns>
307+
public HtmlConversionBehaviorBuilder AddIgnoreResourceHttpStatusDomains(params string[] domains)
308+
{
309+
foreach (var domain in domains)
310+
{
311+
AddIgnoreResourceHttpStatusDomain(domain);
312+
}
313+
314+
return this;
315+
}
316+
317+
/// <summary>
318+
/// Tells Gotenberg to return a 409 Conflict if any resource fails to load due to network errors.
319+
/// </summary>
320+
/// <returns>The builder instance for method chaining.</returns>
321+
public HtmlConversionBehaviorBuilder FailOnResourceLoadingFailed()
322+
{
323+
_htmlConversionBehaviors.FailOnResourceLoadingFailed = true;
324+
325+
return this;
326+
}
327+
177328
/// <summary>
178329
/// Sets the format of the resulting PDF document.
179330
/// </summary>

src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/FacetBase.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
using System.Globalization;
1717

18+
using Gotenberg.Sharp.API.Client.Domain.ValueObjects;
19+
1820
namespace Gotenberg.Sharp.API.Client.Domain.Requests.Facets;
1921

2022
public abstract class FacetBase : IConvertToHttpContent
@@ -78,6 +80,11 @@ public virtual IEnumerable<HttpContent> ToHttpContent()
7880
LibrePdfFormats format => format.ToFormDataValue(),
7981
ConversionPdfFormats format => format.ToFormDataValue(),
8082
List<Cookie> cookies => JsonConvert.SerializeObject(cookies),
83+
List<EmulatedMediaFeature> features => JsonConvert.SerializeObject(
84+
features.ToDictionary(f => f.Name, f => f.Value)),
85+
List<HttpStatusCode> codes => JsonConvert.SerializeObject(codes.Select(c => c.Value)),
86+
List<DomainName> domains => JsonConvert.SerializeObject(domains.Select(d => d.Value)),
87+
CssSelector selector => selector.Value,
8188
float f => f.ToString(cultureInfo),
8289
double d => d.ToString(cultureInfo),
8390
decimal c => c.ToString(cultureInfo),

src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/HtmlConversionBehaviors.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
using Gotenberg.Sharp.API.Client.Domain.ValueObjects;
17+
1618
using Newtonsoft.Json.Linq;
1719

1820
namespace Gotenberg.Sharp.API.Client.Domain.Requests.Facets;
@@ -85,4 +87,44 @@ public class HtmlConversionBehaviors : FacetBase
8587
/// </summary>
8688
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.SkipNetworkIdleEvent)]
8789
public bool? SkipNetworkIdleEvent { get; set; }
90+
91+
/// <summary>
92+
/// CSS selector to wait for before conversion. Delays until the element appears in the DOM.
93+
/// </summary>
94+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.WaitForSelector)]
95+
public CssSelector? WaitForSelector { get; set; }
96+
97+
/// <summary>
98+
/// Overrides CSS media features (e.g., prefers-color-scheme, prefers-reduced-motion).
99+
/// Sent as a JSON array of {name, value} objects.
100+
/// </summary>
101+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.EmulatedMediaFeatures)]
102+
public List<EmulatedMediaFeature>? EmulatedMediaFeatures { get; set; }
103+
104+
/// <summary>
105+
/// HTTP status codes that trigger a 409 Conflict response from Gotenberg
106+
/// when the main page returns a matching code. Default: [499, 599].
107+
/// </summary>
108+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.FailOnHttpStatusCodes)]
109+
public List<HttpStatusCode>? FailOnHttpStatusCodes { get; set; }
110+
111+
/// <summary>
112+
/// HTTP status codes that trigger a failure when page resources (CSS, images, fonts)
113+
/// return a matching code.
114+
/// </summary>
115+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.FailOnResourceHttpStatusCodes)]
116+
public List<HttpStatusCode>? FailOnResourceHttpStatusCodes { get; set; }
117+
118+
/// <summary>
119+
/// Domains to exclude from HTTP status code checks on resources.
120+
/// Useful for ignoring third-party CDNs or analytics domains.
121+
/// </summary>
122+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.IgnoreResourceHttpStatusDomains)]
123+
public List<DomainName>? IgnoreResourceHttpStatusDomains { get; set; }
124+
125+
/// <summary>
126+
/// Tells Gotenberg to return a 409 Conflict if any resource fails to load due to network errors.
127+
/// </summary>
128+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.FailOnResourceLoadingFailed)]
129+
public bool? FailOnResourceLoadingFailed { get; set; }
88130
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2019-2026 Chris Mohan, Jaben Cargman
2+
// and GotenbergSharpApiClient Contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
namespace Gotenberg.Sharp.API.Client.Domain.ValueObjects;
17+
18+
/// <summary>
19+
/// Represents a validated CSS selector string used by Chromium's waitForSelector feature.
20+
/// Delays conversion until the specified element appears in the DOM.
21+
/// </summary>
22+
public sealed class CssSelector : IEquatable<CssSelector>
23+
{
24+
public string Value { get; }
25+
26+
private CssSelector(string value)
27+
{
28+
Value = value;
29+
}
30+
31+
/// <summary>
32+
/// Creates a new CssSelector from the given string.
33+
/// </summary>
34+
/// <param name="selector">A non-empty CSS selector string (e.g., "#content", ".loaded", "[data-ready]").</param>
35+
/// <exception cref="ArgumentException">Thrown when the selector is null or whitespace.</exception>
36+
public static CssSelector Create(string selector)
37+
{
38+
if (string.IsNullOrWhiteSpace(selector))
39+
throw new ArgumentException("CSS selector must not be null or empty.", nameof(selector));
40+
41+
return new CssSelector(selector);
42+
}
43+
44+
public override string ToString() => Value;
45+
46+
public bool Equals(CssSelector? other) => other is not null && Value == other.Value;
47+
48+
public override bool Equals(object? obj) => Equals(obj as CssSelector);
49+
50+
public override int GetHashCode() => Value.GetHashCode();
51+
52+
public static implicit operator string(CssSelector selector) => selector.Value;
53+
54+
public static bool operator ==(CssSelector? left, CssSelector? right) => Equals(left, right);
55+
56+
public static bool operator !=(CssSelector? left, CssSelector? right) => !Equals(left, right);
57+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2019-2026 Chris Mohan, Jaben Cargman
2+
// and GotenbergSharpApiClient Contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
namespace Gotenberg.Sharp.API.Client.Domain.ValueObjects;
17+
18+
/// <summary>
19+
/// Represents a validated domain name used for Gotenberg's ignoreResourceHttpStatusDomains field.
20+
/// Domains matching this list are excluded from HTTP status code failure checks.
21+
/// </summary>
22+
public sealed class DomainName : IEquatable<DomainName>
23+
{
24+
public string Value { get; }
25+
26+
private DomainName(string value)
27+
{
28+
Value = value;
29+
}
30+
31+
/// <summary>
32+
/// Creates a validated domain name.
33+
/// </summary>
34+
/// <param name="domain">A non-empty domain string (e.g., "cdn.example.com", "fonts.googleapis.com").</param>
35+
/// <exception cref="ArgumentException">Thrown when domain is null or whitespace.</exception>
36+
public static DomainName Create(string domain)
37+
{
38+
if (string.IsNullOrWhiteSpace(domain))
39+
throw new ArgumentException("Domain name must not be null or empty.", nameof(domain));
40+
41+
return new DomainName(domain.Trim());
42+
}
43+
44+
public override string ToString() => Value;
45+
46+
public bool Equals(DomainName? other) => other is not null
47+
&& string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
48+
49+
public override bool Equals(object? obj) => Equals(obj as DomainName);
50+
51+
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
52+
53+
public static implicit operator string(DomainName domain) => domain.Value;
54+
55+
public static bool operator ==(DomainName? left, DomainName? right) => Equals(left, right);
56+
57+
public static bool operator !=(DomainName? left, DomainName? right) => !Equals(left, right);
58+
}

0 commit comments

Comments
 (0)