باختصار
تعليق قصير على حكم محكمة إنجليزية بشأن الشرطة. كل قضية وقانون ومقالة يستند إليها مذكورة في حاشية مرقّمة في أسفل الصفحة، بالأسلوب الذي تستعمله كليات الحقوق البريطانية.
ما الذي ستنضده
مستلّة من سلسلة التعليقات على الأحكام في مجلة قانونية: أربع صفحات عن Robinson v Chief Constable of West Yorkshire Police، حكم المحكمة العليا سنة 2018 في متى تقع على الشرطة واجب العناية. الصفحة بقياس 162 × 240 مم، معكوسة، بخط Libre Baskerville بحجم 9.4/13.6 نقطة، وتبدأ الصفحة الأولى بشريط أحمر عنابي عليه اسم القضية منضّدًا بحروف مائلة بحجم 60 نقطة. يكتب المؤلف كل مرجع مفتاحًا مع موضعه الدقيق، [@caparo1990, 617–18]، فينضّد Postext ثلاثًا وعشرين حاشية سفلية وفق OSCOLA، معيار الاستشهاد في كليات الحقوق البريطانية: أسماء القضايا مائلة، والقوانين بخط قائم، ولا نقاط بعد AC أو HL أو edn، وibid لمرجع الحاشية السابقة، وRobinson (n 1) [55] بعد ذلك. المراجع حقيقية؛ أما المؤلف والمجلة فلا.
تجيب هذه الوصفة عن
- كيف أستشهد بالقضايا والتشريعات في الحواشي السفلية، بأسلوب OSCOLA في الكتابة القانونية البريطانية؟
- كيف أنضد الاستشهادات حواشي سفلية، بأسلوب Chicago، مع صيغ مختصرة للاستشهادات اللاحقة؟
الجواب المختصر
// The text cites with [@robinson2018, para 21]; OSCOLA is a note style, so each citation
// becomes a footnote: the case in full the first time, "ibid" when the note before cites
// the same authority, "Robinson (n 1) [55]" after that. Case names come out italic,
// statutes roman, and no abbreviation takes a full stop (AC, HL, UKSC, edn, n, s).
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
style: 'custom', customStyle: OSCOLA, // the bundled 'oscola', patched above
locale: 'en-GB', // OSCOLA's words stay English in the Spanish edition too
notes: 'footnote',
link: false, // a case comment has no bibliography to link to
bibliography: { auto: false }, // OSCOLA articles cite in the notes only
};
// The citation notes share the numbering and the look of the author's own [^notes].
const footnotes = {
placement: 'column', numbering: 'chapter',
fontSize: pt(7.8), lineHeight: pt(10.2), spaceBetween: pt(2.2),
textAlign: 'left', // ragged: long report citations would open wide gaps if justified
separator: { width: 0.25, lineWidth: pt(0.5), color: col('rule') },
};
المكونات
- الميزات
- الاستشهادات في الحواشيالاستشهادات بأسلوب توثيقالحواشي السفليةشريط فصل بعرض الصفحةصفحات افتتاح مصمَّمةسمات العنوانالترويسات وأرقام الصفحاتهوامش متناظرةأنماط الفقراتلوحة ألوان دلاليةالتصدير إلى PDF
- تستخدم أيضًا
- الإحالاتقائمة المراجع من البيانات المرجعيةلون الورقالترويسات بحسب دور الصفحةالخطوط المضمَّنة في PDF
- الخطوط
- Libre Baskerville, Libre Caslon Display, Libre Franklin (SIL OFL 1.1)
- الأصول
- لا شيء: كل صورة مرسومة بالكود
طريقة التحضير
#1 · حاشية OSCOLA لكل مرجع
الشيفرة هي الجواب المختصر أعلاه. OSCOLA أسلوب حواشٍ، فيترك [@robinson2018, para 21] رقمًا مرفوعًا بعد النقطة وتذهب كلماته إلى أسفل الصفحة. تعطي الحاشية 1 القضية كاملة، Robinson v Chief Constable of West Yorkshire Police [2018] UKSC 4, [2018] AC 736؛ والحاشية 2، التي تستشهد بها مجددًا على الفور، نصها ibid [21]؛ والحاشية 8، بعد أن تخللتها قضايا أخرى، نصها Robinson (n 1) [26–27]. ومقالة المجلة القانونية لا تستشهد إلا في حواشيها، فيُبعد bibliography.auto: false قائمة الأعمال عن الصفحة الأخيرة.
#2 · القضية بيانات، تُكتب كما تطبعها مجموعات الأحكام
القضية مادة CSL من type: legal_case: اسمها في title، والمحكمة في authority، وسلسلة مجموعة الأحكام في container-title، والصفحة الأولى في page. يضع OSCOLA سنة المجموعة بين قوسين معقوفين حين تكون السنة هي ما يحدد المجلد، كما في Appeal Cases، لذا تكتب الوصفة تلك السنة في volume يليها رقم المجلد: volume: "[1990] 2" يعطي [1990] 2 AC 605 (HL). والقضية التي حُكم فيها بعد 2001 لها أيضًا استشهاد محايد: authority: UKSC وnumber: "4" وissued: 2018 تعطي [2018] UKSC 4، يليها موضعها في المجموعة. والقانون من type: legislation وسنته في عنوانه بلا issued، فلا تُطبع السنة مرتين، و[@hra1998, § 6] يستشهد بمادة في صورة s 6.
#3 · تعديلان على النمط المرفق
// The bundled oscola.csl writes a paragraph pinpoint in brackets only in a case's first
// note: a later note printed "Robinson (n 1) para 55" and "ibid 27", and a repeated
// statute "ibid 7". These two replacements give "Robinson (n 1) [55]", "ibid [27]" and
// "ibid s 7", as OSCOLA prints them.
const LOCATOR = `<macro name="label-locator"><choose>
<if type="legal_case" locator="paragraph">
<text variable="locator" prefix="[" suffix="]"/></if>
<else-if locator="page"><text variable="locator"/></else-if>
<else><group delimiter=" "><label form="short" strip-periods="true" variable="locator"/>
<text variable="locator"/></group></else>
</choose></macro>`;
const OSCOLA = STYLES.oscola
.replace(/<macro name="label-locator">[\s\S]*?<\/macro>/, LOCATOR)
.replace(/(<if position="ibid-with-locator">[\s\S]*?)<text variable="locator"\/>/,
'$1<text macro="label-locator"/>');
ملف oscola.csl المرفق لا يضع الموضع الدقيق للفقرة بين قوسين إلا في الحاشية الأولى للقضية. فطبعت الحواشي اللاحقة para 55 وibid 27، وفقدت حاشية ثانية إلى القانون نفسه حرف s. يستبدل التعديل ماكرو واحدًا وسطرًا واحدًا من XML النمط قبل البناء ويمرّر الناتج نمطًا مخصّصًا، فلا يتغير النص ولا المراجع.
#4 · اسم القضية على الشريط
const BAND = 124; // mm from the trim's top
const big = (id, content, family, size, color, y, extra = {}) => ({
kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight: 1.08, color,
align: 'left', overflow: 'wrap', ...extra,
placement: { ...at('container', 'top-left', 0, y), size: { width: mm(118), height: 'auto' } },
});
const opener = {
enabled: true,
minHeight: mm(BAND - MARGIN.top + 8), // the text starts 8 mm under the band
slot: { elements: [
{ kind: 'box', id: 'band', style: { backgroundColor: col('claret') },
placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } },
big('kicker', '{attr.kicker}', LABEL, 7.4, col('tint'), 2,
{ fontWeight: 600, letterSpacing: pt(1.4), textTransform: 'uppercase' }),
big('case', '{attr.case}', TEXT, 60, col('paper'), 9, { italic: true }),
big('parties', '{attr.parties}', TEXT, 12.5, col('tint'), 33, { italic: true }),
big('citation', '{attr.citation}', TEXT, 10.5, col('tint'), 40),
{ kind: 'box', id: 'hairline', style: { backgroundColor: col('tint') },
placement: { ...at('container', 'top-left', 0, 51),
size: { width: mm(30), height: pt(0.6) } } },
big('title', '{titleText}', DISPLAY, 27, col('paper'), 57),
big('byline', '{attr.byline}', LABEL, 8.5, col('tint'), 82,
{ fontWeight: 500, letterSpacing: pt(0.5) }),
] },
};
يمتد الشريط 124 مم نزولًا من حدّ القص. اسم القضية والأطراف والاستشهاد سمات للعنوان، فلا يغيّر تعليق آخر في السلسلة إلا Markdown الخاص به. ويُنضَّد Robinson بخط Libre Baskerville Italic بحجم 60 نقطة لأن OSCOLA يميل أسماء القضايا، ويبقى الاستشهاد تحته قائمًا، كما هو في الحواشي.
const palette = {
ink: '#1f1b1a', claret: '#5e1b26', tint: '#ecdcd8', rule: '#bfb4ad', muted: '#6b625d',
paper: '#fcfaf6',
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.claret })
.map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
الوصفة كاملة
// ═══ Postext Cookbook · Nº 099 · A legal opinion cited in OSCOLA ══════════════════ // https://postext.dev/en/cookbook/legal-opinion-oscola // Code: MIT · Text: original (CC BY 4.0) · Pictures: none // Fonts: Libre Baskerville, Libre Caslon Display, Libre Franklin (OFL 1.1) · Needs postext ≥ 1.12.1 import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine, } from 'https://esm.sh/postext'; import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf'; import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'legal-opinion-oscola'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: a law-report claret for the band, ink and cream paper for the rest const palette = { ink: '#1f1b1a', claret: '#5e1b26', tint: '#ecdcd8', rule: '#bfb4ad', muted: '#6b625d', paper: '#fcfaf6', }; // A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs). const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = Object.entries({ ...palette, 'main-color': palette.claret }) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const TEXT = 'Libre Baskerville', DISPLAY = 'Libre Caslon Display', LABEL = 'Libre Franklin'; const TRIM = { width: 162, height: 240 }; // a law-journal offprint const MARGIN = { top: 23, bottom: 22, inner: 19, outer: 21 }; const LEAD = 13.6; // pt const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); // #region oscola: two patches to the bundled OSCOLA style, applied before the build // The bundled oscola.csl writes a paragraph pinpoint in brackets only in a case's first // note: a later note printed "Robinson (n 1) para 55" and "ibid 27", and a repeated // statute "ibid 7". These two replacements give "Robinson (n 1) [55]", "ibid [27]" and // "ibid s 7", as OSCOLA prints them. const LOCATOR = `<macro name="label-locator"><choose> <if type="legal_case" locator="paragraph"> <text variable="locator" prefix="[" suffix="]"/></if> <else-if locator="page"><text variable="locator"/></else-if> <else><group delimiter=" "><label form="short" strip-periods="true" variable="locator"/> <text variable="locator"/></group></else> </choose></macro>`; const OSCOLA = STYLES.oscola .replace(/<macro name="label-locator">[\s\S]*?<\/macro>/, LOCATOR) .replace(/(<if position="ibid-with-locator">[\s\S]*?)<text variable="locator"\/>/, '$1<text macro="label-locator"/>'); // #endregion // #region answer: OSCOLA footnotes for cases, statutes and articles // The text cites with [@robinson2018, para 21]; OSCOLA is a note style, so each citation // becomes a footnote: the case in full the first time, "ibid" when the note before cites // the same authority, "Robinson (n 1) [55]" after that. Case names come out italic, // statutes roman, and no abbreviation takes a full stop (AC, HL, UKSC, edn, n, s). registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES })); const citations = { style: 'custom', customStyle: OSCOLA, // the bundled 'oscola', patched above locale: 'en-GB', // OSCOLA's words stay English in the Spanish edition too notes: 'footnote', link: false, // a case comment has no bibliography to link to bibliography: { auto: false }, // OSCOLA articles cite in the notes only }; // The citation notes share the numbering and the look of the author's own [^notes]. const footnotes = { placement: 'column', numbering: 'chapter', fontSize: pt(7.8), lineHeight: pt(10.2), spaceBetween: pt(2.2), textAlign: 'left', // ragged: long report citations would open wide gaps if justified separator: { width: 0.25, lineWidth: pt(0.5), color: col('rule') }, }; // #endregion // #region opener: the claret band holds the case name, its citation and the title const BAND = 124; // mm from the trim's top const big = (id, content, family, size, color, y, extra = {}) => ({ kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight: 1.08, color, align: 'left', overflow: 'wrap', ...extra, placement: { ...at('container', 'top-left', 0, y), size: { width: mm(118), height: 'auto' } }, }); const opener = { enabled: true, minHeight: mm(BAND - MARGIN.top + 8), // the text starts 8 mm under the band slot: { elements: [ { kind: 'box', id: 'band', style: { backgroundColor: col('claret') }, placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } }, big('kicker', '{attr.kicker}', LABEL, 7.4, col('tint'), 2, { fontWeight: 600, letterSpacing: pt(1.4), textTransform: 'uppercase' }), big('case', '{attr.case}', TEXT, 60, col('paper'), 9, { italic: true }), big('parties', '{attr.parties}', TEXT, 12.5, col('tint'), 33, { italic: true }), big('citation', '{attr.citation}', TEXT, 10.5, col('tint'), 40), { kind: 'box', id: 'hairline', style: { backgroundColor: col('tint') }, placement: { ...at('container', 'top-left', 0, 51), size: { width: mm(30), height: pt(0.6) } } }, big('title', '{titleText}', DISPLAY, 27, col('paper'), 57), big('byline', '{attr.byline}', LABEL, 8.5, col('tint'), 82, { fontWeight: 500, letterSpacing: pt(0.5) }), ] }, }; // #endregion const head = (id, content, parity, edge, x, align) => ({ kind: 'text', id, content, parity, pages: 'body', fontFamily: LABEL, fontWeight: 500, fontSize: pt(7.2), letterSpacing: pt(1), textTransform: 'uppercase', color: col('muted'), align, placement: at('page', edge, x, 13), }); const journal = t({ en: 'Notes on Obligations', es: 'Cuadernos de Obligaciones' }); const config = () => ({ // a factory: the engine caches resolved configs per object locale: t({ en: 'en-us', es: 'es' }), // exact codes (gotcha: hyphenation-locales) colorPalette, citations, footnotes, page: { sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true }, }, layout: { layoutType: 'single' }, bodyText: { fontFamily: TEXT, fontSize: pt(9.4), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: mm(4.5), indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true, avoidWidows: true, avoidOrphans: true, avoidRunts: true, }, headings: { fontFamily: TEXT, fontWeight: 400, color: col('ink'), levels: [ // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break). { level: 1, fontSize: pt(27), breakBefore: { enabled: true, parity: 'any' }, advancedDesign: opener }, { level: 2, italic: true, fontSize: pt(12), color: col('claret'), lineHeight: pt(LEAD), marginTop: pt(LEAD), marginBottom: pt(LEAD / 2) }, ], }, paragraphStyles: [ { id: 'colophon', fontFamily: LABEL, fontSize: pt(6.8), lineHeight: pt(9.2), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) }, ], header: { elements: [ head('verso-folio', '{pageNumber}', 'even', 'top-left', MARGIN.outer, 'left'), head('verso-journal', journal, 'even', 'top-left', MARGIN.outer + 8, 'left'), head('recto-title', '{title}', 'odd', 'top-right', -(MARGIN.outer + 8), 'right'), head('recto-folio', '{pageNumber}', 'odd', 'top-right', -MARGIN.outer, 'right'), ] }, footer: { elements: [ // a drop folio on the opener page only { kind: 'text', id: 'drop-folio', content: '{pageNumber}', pages: 'opener', fontFamily: LABEL, fontWeight: 500, fontSize: pt(7.2), color: col('muted'), align: 'center', placement: at('page', 'bottom', 0, -11) }, ] }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---نموذج Markdown · أسطر: 192 · content.en.md
title: "Acts, Omissions and the Police" author: "Harriet Okafor" --- # Acts, Omissions and the Police {#comment kicker="Notes on Obligations · Case Comment" case="Robinson" parties="v Chief Constable of West Yorkshire Police" citation="[2018] UKSC 4, [2018] AC 736" byline="Harriet Okafor"} On a July afternoon in 2008 two detectives tried to arrest a man suspected of dealing drugs on a shopping street in the centre of Huddersfield. He resisted, and the three men fell against Mrs Robinson, a woman of 76 who was walking past, and knocked her to the ground. The trial judge found that the officers had been careless in attempting the arrest where and when they did, but held that the police owed no duty of care to a passer-by hurt in the course of their work against crime. The Court of Appeal agreed. The Supreme Court did not.[@robinson2018] Lord Reed, with whom Lady Hale and Lord Hodge agreed, began by correcting a reading of the law that had gone uncorrected for almost thirty years.[@robinson2018, para 21] This comment argues that the case matters less for what it decided about the police than for the method it restored. A court asked whether a duty of care exists should look first for an established category and reason from it by analogy; it should weigh questions of policy only when the case is new. ## There is no “Caparo test” The modern law begins with Lord Atkin’s statement that a person must take reasonable care to avoid acts or omissions which he can reasonably foresee would be likely to injure his neighbour.[@donoghue1932, 580] In the 1970s the House of Lords tried to turn that statement into a general formula. Lord Wilberforce proposed two stages: a relationship of proximity in which harm was foreseeable, and then any considerations that ought to negative or limit the duty.[@anns1978, 751–52] The formula expanded liability faster than the courts could defend, and *Anns* was overruled in 1990.[@murphy1991] In the same year Lord Bridge described the ingredients of a duty as foreseeability of damage, a relationship of proximity, and a situation in which the court considers it fair, just and reasonable to impose a duty.[@caparo1990, 617–18] Many judges went on to apply it as a three-stage test to every claim, including claims for physical injury caused by a positive act. Lord Bridge had said the opposite. He doubted that any single test could settle every case and preferred that the law develop novel categories incrementally.[^brennan] [^brennan]: Lord Bridge adopted the words of Brennan J in the High Court of Australia; see [@caparo1990, 618; @sutherland1985, 481] *Robinson* restores that reading. Where a case falls within an established category, the court applies the law as settled; the question of what is fair, just and reasonable arises only where the court is asked to go further.[@robinson2018, para 26–27] Physical injury caused by a careless positive act has been an established category since *Donoghue*. The officers’ decision to make the arrest in a crowded street was such an act, the risk to people nearby was foreseeable, and nothing more was needed to found a duty.[@robinson2018, para 74] ## Acts and omissions The common law does not, in general, impose a duty to protect another from harm caused by a third party or by nature.[@smith1987, 271] Lord Hoffmann gave the reason in political terms: it is less of an invasion of a person’s freedom to require him to take care when he chooses to act than to require him to act at all, and a duty to rescue invites the question why it should fall on one bystander rather than another.[@stovin1996, 943–44] The rule has exceptions: where the defendant has assumed responsibility, controls the source of the danger, or has created it. Read in this light, the earlier police cases were cases about omissions. In *Hill* the mother of the last victim of Peter Sutcliffe sued the force that had failed to catch him. The House of Lords held that no duty was owed to her daughter, who was one of an indeterminate public, and added reasons of policy about the effect of liability on police work.[@hill1989, 63] For thirty years *Hill* was cited as if it gave the police a general immunity. Lord Reed treated the policy reasons as a supplement to the ordinary rule: the police, like anyone else, owe no duty to prevent harm by a third party, unless one of the exceptions applies; but they owe the ordinary duty not to injure people by what they themselves do.[@robinson2018, para 55] The distinction had already decided *Michael*, where a call to the emergency services was wrongly graded and the caller was killed before officers arrived. The majority held that the police had assumed no responsibility for her safety and so owed her no duty in negligence.[@michael2015, para 97–100] Academic opinion had divided on the point. One view holds that a public body set up to protect people should bear a narrow duty where it knows of a specific threat to a specific person;[^peripheral] the other, that the omissions rule applies to public authorities as it applies to private persons, and that any wider duty is for Parliament.[@nolan2011] [^peripheral]: [@tofaris2016, 130–32; cf @stapleton1995, 304–06, on the deterrent value of claims against parties who are not the primary wrongdoer] ## The statutory route Section 6(1) of the Human Rights Act 1998 makes it unlawful for a public authority to act in a way that is incompatible with a Convention right,[@hra1998, § 6] and a victim may bring proceedings against it under section 7.[@hra1998, § 7] In *Michael* the claim under article 2 of the Convention was allowed to go to trial even though the claim in negligence failed.[@michael2015, para 139] In *DSD* the Supreme Court held the police liable under article 3 for serious failures in investigating a series of sexual assaults by a taxi driver.[@dsd2018, para 24] The concern behind *Hill*, that liability would push the police into defensive practices, has a statutory answer of its own. When a court decides what steps a defendant should have taken to meet the standard of care, it may consider whether requiring those steps might prevent a desirable activity from being undertaken.[@compensation2006, § 1] That is a question of breach, not of duty. The officers in *Robinson* were not expected to stop arresting suspects, only to choose the moment with some regard for the people on the pavement. ## Comment A year later the Supreme Court applied the same method to a local authority that had failed to rehouse a family harassed by their neighbours, and found no duty, because the council had neither created the danger nor assumed responsibility for the children’s safety.[@poole2019, para 64–65] The two decisions point in different directions on their facts and rest on the same reasoning. Two difficulties remain. The line between acts and omissions is easy to state and hard to draw: an officer who opens a door and lets a violent man in has acted, but one who fails to lock it has not, and the difference may be small to the person on the other side. And the exceptions do much of the work. An assumption of responsibility can be found in a promise, a practice or the nature of a service, and the cases since *Robinson* show that the courts have not agreed how readily to find one.[^generally] The case does not resolve either difficulty; it puts them where they belong, in the analysis of established categories, and not in an open question about what is fair. [^generally]: For the exceptions and the cases on each, see generally [@winfield2020] :::paragraphs{style="colophon"} Written for the Postext Cookbook (CC BY 4.0). Author and series are fictional; this is not legal advice. The authorities are real; some pinpoints are illustrative. Set in Libre Baskerville, Libre Caslon Display and Libre Franklin (SIL OFL). ::: :::references{format=csl-yaml} - id: donoghue1932 type: legal_case title: Donoghue v Stevenson title-short: Donoghue volume: "[1932]" container-title: AC page: "562" authority: HL - id: anns1978 type: legal_case title: Anns v Merton London Borough Council title-short: Anns volume: "[1978]" container-title: AC page: "728" authority: HL - id: murphy1991 type: legal_case title: Murphy v Brentwood District Council title-short: Murphy volume: "[1991] 1" container-title: AC page: "398" authority: HL - id: caparo1990 type: legal_case title: Caparo Industries plc v Dickman title-short: Caparo volume: "[1990] 2" container-title: AC page: "605" authority: HL - id: sutherland1985 type: legal_case title: Sutherland Shire Council v Heyman title-short: Sutherland volume: "157" container-title: CLR page: "424" authority: HCA issued: 1985 - id: smith1987 type: legal_case title: Smith v Littlewoods Organisation Ltd title-short: Smith volume: "[1987]" container-title: AC page: "241" authority: HL - id: stovin1996 type: legal_case title: Stovin v Wise title-short: Stovin volume: "[1996]" container-title: AC page: "923" authority: HL - id: hill1989 type: legal_case title: Hill v Chief Constable of West Yorkshire title-short: Hill volume: "[1989]" container-title: AC page: "53" authority: HL - id: michael2015 type: legal_case title: Michael v Chief Constable of South Wales Police title-short: Michael authority: UKSC number: "2" issued: 2015 volume: "[2015]" container-title: AC page: "1732" - id: robinson2018 type: legal_case title: Robinson v Chief Constable of West Yorkshire Police title-short: Robinson authority: UKSC number: "4" issued: 2018 volume: "[2018]" container-title: AC page: "736" - id: dsd2018 type: legal_case title: Commissioner of Police of the Metropolis v DSD title-short: DSD authority: UKSC number: "11" issued: 2018 volume: "[2019]" container-title: AC page: "196" - id: poole2019 type: legal_case title: N v Poole Borough Council title-short: Poole authority: UKSC number: "25" issued: 2019 volume: "[2020]" container-title: AC page: "780" - id: hra1998 type: legislation title: Human Rights Act 1998 - id: compensation2006 type: legislation title: Compensation Act 2006 - id: stapleton1995 type: article-journal author: [{family: Stapleton, given: Jane}] title: "Duty of Care: Peripheral Parties and Alternative Opportunities for Deterrence" container-title: LQR volume: "111" page: "301" issued: 1995 - id: nolan2011 type: article-journal author: [{family: Nolan, given: Donal}] title: The Liability of Public Authorities for Failing to Confer Benefits container-title: LQR volume: "127" page: "260" issued: 2011 - id: tofaris2016 type: article-journal author: [{family: Tofaris, given: Stelios}, {family: Steel, given: Sandy}] title: Negligence Liability for Omissions and the Police container-title: CLJ volume: "75" page: "128" issued: 2016 - id: winfield2020 type: book author: [{family: Goudkamp, given: James}, {family: Nolan, given: Donal}] title: Winfield and Jolowicz on Tort edition: 20 publisher: Sweet & Maxwell issued: 2020 :::`; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses (gotcha: fonts-first). const FONTS = { 'Libre Baskerville': ['400', '400i', '700'], 'Libre Caslon Display': ['400'], 'Libre Franklin': ['400', '500', '600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown); const title = t({ en: 'A legal opinion cited in OSCOLA', es: 'Un dictamen jurídico citado en OSCOLA' }); showPages(doc, { title }); offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);العُدّة · core, fonts, viewer, pdf: نفسها في كل وصفة · أسطر: 275
// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook ───────── function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v1 ── the same in every recipe · postext.dev/cookbook ──────── // Postext measures text with the faces the browser has loaded, and caches the // widths, so every face must be ready before the first build. Faces come from // Fontsource: the same static files the PDF embeds, so screen and PDF agree. /** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With * `optional`, a face Fontsource does not ship is skipped instead of failing. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', }; const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin']; const jobs = []; let added = 0; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const meta = optional ? await fontsourceMeta(family) : null; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; if (hasFace(family, weight, style)) continue; if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` (a buildDocument or buildBundle call) and checks the faces * the pages use. A regular face missing from FONTS is loaded with a warning; * bold and italic variants are loaded when the family ships them. Then the * measurement caches are cleared and the build runs again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout. `base` marks a block's own face; its * bold, italic and bold-italic variants are listed whether or not used. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** True when a loaded FontFace covers exactly this family, weight and style * (document.fonts.check() is also true for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v1 ── the same in every recipe · postext.dev/cookbook ─────── /** Shows the pages as facing spreads on a dark desk: the first page is a * recto on its own, then verso | recto pairs, as in a bound book. Pages * are painted when they scroll near the screen. */ function showPages(docs, { title, width = 460 } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · pdf v1 ── the same in every recipe that exports a PDF ────────────── /** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen * used, snapping to a weight the family ships and falling back to upright * when it has no italic: the PDF asks for every face a block could use. */ async function fontsourceProvider(family, weight, style) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`); return decompressWoff2(new Uint8Array(await res.arrayBuffer())); } /** A "Build the PDF" button in the bar. Once built: "Open the PDF" (a new * tab, since CodePen's preview frame cannot show PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── /Kit ───────────────────────────────────────────────────────────────────────
يعمل ملف script.js المجمّع كما هو: الصقه في سكربت الوحدة (module) لأي صفحة، أو افتح الوصفة على CodePen. مجلد الوصفة على GitHub ↗ (يفتح في تبويب جديد)
تنويعات
#اطبع النمط كما يأتي
للمقارنة مع الناتج غير المعدَّل، استعمل المعرّف المرفق. عندئذٍ تقرأ المواضع الدقيقة للفقرات في الحواشي اللاحقة para 55.
- style: 'custom', customStyle: OSCOLA, // the bundled 'oscola', patched above
+ style: 'oscola',#اجمع الحواشي في النهاية
بعض المجلات تطبع حواشيها بعد النص. تحتفظ الاستشهادات بصيغتيها الكاملة والمختصرة وبأرقامها.
- placement: 'column', numbering: 'chapter',
+ placement: 'chapterEnd', numbering: 'chapter',أخطاء شائعة
خطأ شائع
أي كائن headings يُلغي فاصل الصفحة قبل H1
ينتقل H1 افتراضيًا إلى صفحة فردية (always-odd)، لكن تمرير أي كائن headings يعيد ضبط هذا الافتراض، فتتوالى الفصول دون فاصل ولا يفعل span: 'page' شيئًا. أعد كتابة headings.levels[0].breakBefore: { enabled: true, parity } في كل إعداد. فصول تبدأ في صفحة فردية →
خطأ شائع
حمّل كل أوجه الخط قبل الإخراج
يقيس الإخراج النص بأوجه الخط التي حمّلها المتصفح ويخزّن العروض مؤقتًا، فالوجه الذي يصل بعد البناء الأول يترك فواصل أسطر خاطئة وملف PDF لم يعد يطابق الشاشة. حمّل كل وزن وكل نمط أولًا، واستدعِ clearMeasurementCache() قبل إعادة البناء إذا تأخر وصول أحدها. الخطوط قبل الإخراج →
خطأ شائع
لوحة الألوان المستبدلة لا تصل إلى عناصر التصميم ولا إلى لون الإحالة
يقرأ postext 1.4.1 الإعداد colorPalette في أنماط النص (المتن والعناوين والقوائم والتعليقات والجداول والإطارات) لكن لا في عناصر الترويسات والتذييلات والافتتاحيات وصفحات الأجزاء، ولا في bodyText.referenceColor: تحتفظ بالقيمة الست عشرية المكتوبة بجانب paletteId الخاص بها. حين تستبدل لوحة الألوان، لنسخة شاشة داكنة أو لإعادة تلوين، أعِد كتابة كل لون مرتبط من colorPalette قبل البناء. لوحة ألوان دلالية →
خطأ شائع
8 لغات فقط تُقسَّم بالواصلة، بالرمز المطابق تمامًا
يتوفر تقسيم الكلمات بالواصلة للغات en-us وes وfr وde وit وpt وca وnl، بمطابقة تامة للرمز: 'es-ES' أو أي لغة أخرى تعود دون تنبيه إلى الإنجليزية الأمريكية. تقسيم الكلمات ولغة المستند →
خطأ شائع
يُخزَّن الإعداد مؤقتًا بحسب هويته: ابنِ كائنًا جديدًا
يخزّن المحرّك الإعدادات المحسوبة مؤقتًا بحسب هوية الكائن، فتعديل الإعداد في مكانه ثم البناء مجددًا يعيد استخدام النتيجة القديمة. ابنِ كائنًا جديدًا في كل بناء، ولهذا يكون إعداد الوصفة دالة مصنِّعة: config(). صفحات على اللوحة (Canvas) →
- المجموعة التي مجلدها رقم مجرد،
volume: "2"، تطبع السنة بين قوسين هلاليين، (1990) 2 AC، وهو ما يحفظه OSCOLA للسلاسل المرقّمة عبر السنين مثل Commonwealth Law Reports (الحاشية 7). اكتب السنة بين قوسين معقوفين فيvolumeلـAppeal Cases وWeekly Law Reports. - حرف
sقبل الموضع يعني صفحة (Seite الألمانية)، لا مادة: اكتب§ 6. والفقرة الفرعية مثل6(1)لا تُقرأ موضعًا؛ سمِّها في الجملة واستشهد بالمادة. - لا قائمة مراجع في مقالة وفق OSCOLA، وليس في النمط جداول منفصلة للقضايا والتشريعات. والأطروحة التي تحتاج تلك الجداول تحصل من
:::bibliographyعلى قائمة واحدة مرتبة، المصادر الثانوية أولًا ثم القضايا والقوانين مختلطة.
الحقوق
- الوصفة
- Ignacio Ferro
- النص
- نص أصلي, CC BY 4.0
- الخطوط
- Libre Baskerville (SIL OFL 1.1) · Libre Caslon Display (SIL OFL 1.1) · Libre Franklin (SIL OFL 1.1)
- الكود
- MIT، مثل Postext
حرّر هذا الشرح ↗ (يفتح في تبويب جديد)مجلد الوصفة على GitHub ↗ (يفتح في تبويب جديد)


